using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using AxCopilot.Models;

namespace AxCopilot.Services.Agent;

/// <summary>
/// 확장 훅 러너 (Phase 17-C).
/// 기존 AgentHookRunner(pre/post 스크립트)를 대체·보완.
/// 새 이벤트 종류(SessionStart, FileChanged 등)와 실행 모드(Http, Prompt, Agent) 지원.
/// </summary>
public static class ExtendedHookRunner
{
    private static readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(60) };

    private static readonly JsonSerializerOptions _json = new()
    {
        PropertyNameCaseInsensitive = true,
        Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
    };

    /// <summary>지정 이벤트의 훅을 모두 실행하고 결과를 병합하여 반환.</summary>
    /// <param name="llm">Prompt 모드 훅에서 LLM 호출에 사용할 서비스 (null이면 Prompt 모드 스킵).</param>
    public static async Task<ExtendedHookResult> RunEventAsync(
        IReadOnlyList<ExtendedHookEntry> hooks,
        HookContext context,
        CancellationToken ct = default,
        LlmService? llm = null)
    {
        if (hooks == null || hooks.Count == 0)
            return new ExtendedHookResult();

        var matchingHooks = hooks
            .Where(h => h.Enabled && h.Event == context.Event)
            .Where(h => MatchesTool(h.Matcher, context.ToolName))
            .Where(h => EvaluateCondition(h.Condition, context))
            .ToList();

        if (matchingHooks.Count == 0)
            return new ExtendedHookResult();

        // Once 훅 처리: 실행 후 비활성화 목록 수집
        var toDisable = new List<ExtendedHookEntry>();

        // 비동기 훅: fire-and-forget (asyncRewake는 결과를 모니터링)
        var asyncHooks = matchingHooks.Where(h => h.IsAsync || h.AsyncRewake).ToList();
        foreach (var hook in asyncHooks)
        {
            if (hook.AsyncRewake)
            {
                // Phase 25-B: asyncRewake — 비동기 실행하되 exit code 2 시 rewake 콜백 호출
                _ = Task.Run(async () =>
                {
                    try
                    {
                        var result = await ExecuteSingleAsync(hook, context, CancellationToken.None, llm);
                        if (result.Block)
                        {
                            // exit code 2 = rewake → AdditionalContext를 통해 대화 재활성화
                            AsyncRewakeTriggered?.Invoke(null, new AsyncRewakeEventArgs
                            {
                                HookName = hook.Name,
                                AdditionalContext = result.BlockReason ?? result.AdditionalContext,
                                SessionId = context.SessionId,
                            });
                        }
                    }
                    catch (Exception) { }
                });
            }
            else
            {
                _ = Task.Run(async () =>
                {
                    try { await ExecuteSingleAsync(hook, context, CancellationToken.None, llm); }
                    catch (Exception) { }
                });
            }
            if (hook.Once) toDisable.Add(hook);
        }

        // 동기 훅: 순차 실행, 결과 병합
        var syncHooks = matchingHooks.Where(h => !h.IsAsync).ToList();
        string? combinedContext = null;
        string? updatedInput = null;
        ExtendedPermissionDecision? permDecision = null;
        List<string>? watchPaths = null;
        bool anyBlocked = false;
        string? blockReason = null;

        foreach (var hook in syncHooks)
        {
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
            cts.CancelAfter(TimeSpan.FromSeconds(hook.TimeoutSeconds));

            try
            {
                var result = await ExecuteSingleAsync(hook, context, cts.Token, llm);

                if (result.Block) { anyBlocked = true; blockReason = result.BlockReason; break; }
                if (result.AdditionalContext != null)
                    combinedContext = (combinedContext ?? "") + "\n" + result.AdditionalContext;
                if (result.UpdatedInput != null) updatedInput = result.UpdatedInput;
                if (result.PermissionDecision.HasValue) permDecision = result.PermissionDecision;
                if (result.WatchPaths != null)
                {
                    watchPaths ??= new List<string>();
                    watchPaths.AddRange(result.WatchPaths);
                }
                if (hook.Once) toDisable.Add(hook);
            }
            catch (OperationCanceledException) { }
            catch (Exception) { }
        }

        // Once 훅 비활성화
        foreach (var h in toDisable) h.Enabled = false;

        return new ExtendedHookResult
        {
            Block = anyBlocked,
            BlockReason = blockReason,
            AdditionalContext = combinedContext?.Trim(),
            UpdatedInput = updatedInput,
            PermissionDecision = permDecision,
            WatchPaths = watchPaths,
        };
    }

    private static async Task<ExtendedHookResult> ExecuteSingleAsync(
        ExtendedHookEntry hook,
        HookContext context,
        CancellationToken ct,
        LlmService? llm = null)
    {
        return hook.Mode switch
        {
            HookExecutionMode.Command => await RunCommandHookAsync(hook, context, ct),
            HookExecutionMode.Http    => await RunHttpHookAsync(hook, context, ct),
            // Phase 17-C: Prompt 모드 — LlmService 주입 필요. 없으면 스킵.
            HookExecutionMode.Prompt  => llm != null
                ? await RunPromptHookAsync(hook, context, llm, ct)
                : new ExtendedHookResult(),
            // Agent 모드: AgentLoopService 참조 필요 — 미구현 (향후 Phase 18-A에서 처리)
            HookExecutionMode.Agent   => new ExtendedHookResult(),
            _ => new ExtendedHookResult()
        };
    }

    /// <summary>
    /// Phase 17-C: Prompt 모드 훅 — LLM으로 도구/이벤트를 평가합니다.
    /// 프롬프트에서 {{tool_name}}, {{tool_input}}, {{tool_output}}, {{user_message}} 치환.
    /// 응답에 "block", "deny", "차단" 포함 시 Block=true 반환.
    /// </summary>
    private static async Task<ExtendedHookResult> RunPromptHookAsync(
        ExtendedHookEntry hook,
        HookContext context,
        LlmService llm,
        CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(hook.Prompt)) return new ExtendedHookResult();

        // 변수 치환
        var evalPrompt = hook.Prompt
            .Replace("{{tool_name}}",    context.ToolName    ?? "", StringComparison.OrdinalIgnoreCase)
            .Replace("{{tool_input}}",   context.ToolInput   ?? "", StringComparison.OrdinalIgnoreCase)
            .Replace("{{tool_output}}",  context.ToolOutput  ?? "", StringComparison.OrdinalIgnoreCase)
            .Replace("{{user_message}}", context.UserMessage ?? "", StringComparison.OrdinalIgnoreCase)
            .Replace("{{event}}",        context.Event.ToString(), StringComparison.OrdinalIgnoreCase)
            .Replace("{{session_id}}",   context.SessionId,        StringComparison.OrdinalIgnoreCase);

        string response;
        try
        {
            var messages = new List<ChatMessage>
            {
                new() { Role = "user", Content = evalPrompt }
            };
            // 훅 전용 소형 모델이 지정된 경우 → 현재는 기본 LLM 사용 (향후 모델 오버라이드 지원 예정)
            response = await llm.SendAsync(messages, ct);
        }
        catch (Exception)
        {
            return new ExtendedHookResult();
        }

        if (string.IsNullOrWhiteSpace(response)) return new ExtendedHookResult();

        // 차단 신호 감지
        var lower = response.ToLowerInvariant();
        if (lower.Contains("block") || lower.Contains("deny") || lower.Contains("차단"))
            return new ExtendedHookResult { Block = true, BlockReason = Truncate(response, 500) };

        return new ExtendedHookResult { AdditionalContext = Truncate(response, 2000) };
    }

    private static async Task<ExtendedHookResult> RunCommandHookAsync(
        ExtendedHookEntry hook,
        HookContext context,
        CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(hook.ScriptPath))
            return new ExtendedHookResult();

        var scriptPath = Environment.ExpandEnvironmentVariables(hook.ScriptPath);
        if (!File.Exists(scriptPath))
            return new ExtendedHookResult();

        var ext = Path.GetExtension(scriptPath).ToLowerInvariant();
        string fileName, args;
        switch (ext)
        {
            case ".ps1":
                fileName = "powershell.exe";
                args = $"-NoProfile -ExecutionPolicy Bypass -File \"{scriptPath}\"";
                break;
            case ".bat": case ".cmd":
                fileName = "cmd.exe";
                args = $"/c \"{scriptPath}\"";
                break;
            default:
                return new ExtendedHookResult();
        }
        if (!string.IsNullOrWhiteSpace(hook.Arguments)) args += " " + hook.Arguments;

        var psi = new ProcessStartInfo
        {
            FileName = fileName, Arguments = args,
            WorkingDirectory = context.WorkFolder.Length > 0 ? context.WorkFolder
                : Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
            UseShellExecute = false, CreateNoWindow = true,
            RedirectStandardOutput = true, RedirectStandardError = true,
        };
        SetEnvVars(psi, context);

        using var process = new Process { StartInfo = psi };
        var stdout = new StringBuilder();
        var stderr = new StringBuilder();
        process.OutputDataReceived += (_, e) => { if (e.Data != null) stdout.AppendLine(e.Data); };
        process.ErrorDataReceived  += (_, e) => { if (e.Data != null) stderr.AppendLine(e.Data); };
        process.Start();
        process.BeginOutputReadLine();
        process.BeginErrorReadLine();

        try { await process.WaitForExitAsync(ct); }
        catch (OperationCanceledException) { try { process.Kill(true); } catch (Exception) { } return new ExtendedHookResult(); }

        // exit code 2 → Block
        if (process.ExitCode == 2)
            return new ExtendedHookResult { Block = true, BlockReason = stderr.ToString().Trim().Length > 0 ? stderr.ToString().Trim() : stdout.ToString().Trim() };

        // JSON 출력 파싱
        var output = stdout.ToString().Trim();
        if (output.StartsWith("{"))
        {
            try
            {
                using var doc = JsonDocument.Parse(output);
                var root = doc.RootElement;
                return new ExtendedHookResult
                {
                    AdditionalContext = root.TryGetProperty("additionalContext", out var ac) ? ac.GetString() : null,
                    UpdatedInput      = root.TryGetProperty("updatedInput", out var ui) ? ui.GetString() : null,
                };
            }
            catch (Exception) { }
        }

        return new ExtendedHookResult { AdditionalContext = output.Length > 0 ? output : null };
    }

    private static async Task<ExtendedHookResult> RunHttpHookAsync(
        ExtendedHookEntry hook,
        HookContext context,
        CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(hook.Url)) return new ExtendedHookResult();

        try
        {
            var payload = JsonSerializer.Serialize(new
            {
                @event   = context.Event.ToString(),
                tool_name  = context.ToolName,
                tool_input = context.ToolInput,
                tool_output = context.ToolOutput,
                user_message = context.UserMessage,
                session_id = context.SessionId,
                work_folder = context.WorkFolder,
            }, _json);

            using var req = new HttpRequestMessage(HttpMethod.Post, hook.Url);
            req.Content = new StringContent(payload, Encoding.UTF8, "application/json");
            foreach (var (k, v) in hook.Headers) req.Headers.TryAddWithoutValidation(k, v);

            var resp = await _http.SendAsync(req, ct);
            if (!resp.IsSuccessStatusCode) return new ExtendedHookResult();

            var body = await resp.Content.ReadAsStringAsync(ct);
            if (body.StartsWith("{"))
            {
                using var doc = JsonDocument.Parse(body);
                var root = doc.RootElement;
                var block = root.TryGetProperty("block", out var b) && b.GetBoolean();
                return new ExtendedHookResult
                {
                    Block = block,
                    AdditionalContext = root.TryGetProperty("additionalContext", out var ac) ? ac.GetString() : null,
                    UpdatedInput      = root.TryGetProperty("updatedInput", out var ui) ? ui.GetString() : null,
                };
            }
        }
        catch (Exception) { }
        return new ExtendedHookResult();
    }

    private static void SetEnvVars(ProcessStartInfo psi, HookContext context)
    {
        // Phase 17-C-EXT: HookEnvironmentBuilder로 CC 호환 변수 자동 주입
        var envVars = HookEnvironmentBuilder.Build(context);
        foreach (var (key, value) in envVars)
            psi.EnvironmentVariables[key] = value;

        // 하위 호환: 기존 변수 유지
        psi.EnvironmentVariables["AX_WORK_FOLDER"]  = context.WorkFolder;
        psi.EnvironmentVariables["AX_TOOL_TIMING"]  = context.Event == HookEventKind.PreToolUse ? "pre" : "post";
        psi.EnvironmentVariables["AX_TOOL_SUCCESS"] = context.ToolSuccess ? "true" : "false";
    }

    private static bool MatchesTool(string? matcher, string? toolName)
    {
        if (string.IsNullOrEmpty(matcher) || matcher == "*") return true;
        if (string.IsNullOrEmpty(toolName)) return false;
        return string.Equals(matcher, toolName, StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Phase 25-A: 훅 조건부 실행.
    /// CC의 `if` 필드와 동등. 간단한 조건식을 평가합니다.
    /// 형식: "field == value", "field != value", "field contains value"
    /// 지원 필드: tool_name, event, session_id, work_folder, active_tab
    /// </summary>
    private static bool EvaluateCondition(string? condition, HookContext context)
    {
        if (string.IsNullOrEmpty(condition)) return true;

        // 여러 조건을 && 로 연결 지원
        var parts = condition.Split("&&", StringSplitOptions.TrimEntries);
        return parts.All(part => EvaluateSingleCondition(part, context));
    }

    private static bool EvaluateSingleCondition(string condition, HookContext context)
    {
        // "field operator value" 파싱
        string field, op, value;

        if (condition.Contains("!="))
        {
            var idx = condition.IndexOf("!=", StringComparison.Ordinal);
            field = condition[..idx].Trim();
            op = "!=";
            value = condition[(idx + 2)..].Trim().Trim('"', '\'');
        }
        else if (condition.Contains("=="))
        {
            var idx = condition.IndexOf("==", StringComparison.Ordinal);
            field = condition[..idx].Trim();
            op = "==";
            value = condition[(idx + 2)..].Trim().Trim('"', '\'');
        }
        else if (condition.Contains(" contains ", StringComparison.OrdinalIgnoreCase))
        {
            var idx = condition.IndexOf(" contains ", StringComparison.OrdinalIgnoreCase);
            field = condition[..idx].Trim();
            op = "contains";
            value = condition[(idx + 10)..].Trim().Trim('"', '\'');
        }
        else
        {
            // 파싱 불가 → 안전하게 통과
            return true;
        }

        var fieldValue = ResolveField(field, context) ?? "";

        return op switch
        {
            "==" => fieldValue.Equals(value, StringComparison.OrdinalIgnoreCase),
            "!=" => !fieldValue.Equals(value, StringComparison.OrdinalIgnoreCase),
            "contains" => fieldValue.Contains(value, StringComparison.OrdinalIgnoreCase),
            _ => true
        };
    }

    private static string? ResolveField(string field, HookContext context)
    {
        return field.ToLowerInvariant() switch
        {
            "tool_name" or "toolname" => context.ToolName,
            "event" or "hook_event" => context.Event.ToString(),
            "session_id" or "sessionid" => context.SessionId,
            "work_folder" or "workfolder" or "cwd" => context.WorkFolder,
            "user_message" or "usermessage" => context.UserMessage,
            "changed_file" or "changedfile" => context.ChangedFilePath,
            _ => null
        };
    }

    /// <summary>Phase 25-B: asyncRewake 훅이 완료되어 대화 재활성화가 필요할 때 발생.</summary>
    public static event EventHandler<AsyncRewakeEventArgs>? AsyncRewakeTriggered;

    private static string Truncate(string? value, int max)
        => string.IsNullOrEmpty(value) ? "" : (value.Length <= max ? value : value[..max]);
}

/// <summary>Phase 25-B: asyncRewake 이벤트 인자.</summary>
public class AsyncRewakeEventArgs : EventArgs
{
    public string HookName { get; init; } = "";
    public string? AdditionalContext { get; init; }
    public string SessionId { get; init; } = "";
}
