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

namespace AxCopilot.Services.Agent;

/// <summary>
/// 에이전트 도구 실행 전/후에 사용자 정의 스크립트(bat/ps1/cmd)를 실행하는 훅 러너.
/// <para>
/// 환경 변수로 도구 정보를 전달:
///   AX_TOOL_NAME   — 도구 이름
///   AX_TOOL_TIMING — pre | post
///   AX_TOOL_INPUT  — 도구 입력 JSON (최대 4KB)
///   AX_TOOL_OUTPUT — 도구 출력 (post만, 최대 4KB)
///   AX_TOOL_SUCCESS — true | false (post만)
///   AX_WORK_FOLDER — 작업 폴더 경로
/// </para>
/// </summary>
public static class AgentHookRunner
{
    private const int MaxEnvValueLength = 4096;

    /// <summary>
    /// 지정된 타이밍(pre/post)에 해당하는 훅을 모두 실행합니다.
    /// </summary>
    /// <param name="hooks">설정에 등록된 전체 훅 목록</param>
    /// <param name="toolName">실행된 도구 이름</param>
    /// <param name="timing">"pre" 또는 "post"</param>
    /// <param name="toolInput">도구 입력 JSON</param>
    /// <param name="toolOutput">도구 출력 (post만)</param>
    /// <param name="success">도구 실행 성공 여부 (post만)</param>
    /// <param name="workFolder">작업 폴더 경로</param>
    /// <param name="timeoutMs">스크립트 타임아웃 (밀리초)</param>
    /// <param name="ct">취소 토큰</param>
    /// <returns>각 훅의 실행 결과 (이름, 성공여부, 출력/에러)</returns>
    public static async Task<List<HookExecutionResult>> RunAsync(
        IReadOnlyList<AgentHookEntry> hooks,
        string toolName,
        string timing,
        string? toolInput = null,
        string? toolOutput = null,
        bool success = true,
        string? workFolder = null,
        int timeoutMs = 10000,
        CancellationToken ct = default)
    {
        var results = new List<HookExecutionResult>();
        if (hooks == null || hooks.Count == 0) return results;

        foreach (var hook in hooks)
        {
            if (!hook.Enabled) continue;
            if (!string.Equals(hook.Timing, timing, StringComparison.OrdinalIgnoreCase)) continue;

            // 도구 이름 매칭: "*" = 전체, 그 외 정확 매칭 (대소문자 무시)
            if (hook.ToolName != "*" &&
                !string.Equals(hook.ToolName, toolName, StringComparison.OrdinalIgnoreCase))
                continue;

            var result = await ExecuteHookAsync(hook, toolName, timing, toolInput, toolOutput, success, workFolder, timeoutMs, ct);
            results.Add(result);
        }

        return results;
    }

    private static async Task<HookExecutionResult> ExecuteHookAsync(
        AgentHookEntry hook,
        string toolName,
        string timing,
        string? toolInput,
        string? toolOutput,
        bool success,
        string? workFolder,
        int timeoutMs,
        CancellationToken ct)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(hook.ScriptPath))
                return new HookExecutionResult(hook.Name, false, "스크립트 경로가 비어 있습니다.");

            var scriptPath = Environment.ExpandEnvironmentVariables(hook.ScriptPath);
            if (!File.Exists(scriptPath))
                return new HookExecutionResult(hook.Name, false, $"스크립트를 찾을 수 없습니다: {scriptPath}");

            var ext = Path.GetExtension(scriptPath).ToLowerInvariant();
            string fileName;
            string arguments;

            switch (ext)
            {
                case ".ps1":
                    fileName = "powershell.exe";
                    arguments = $"-NoProfile -ExecutionPolicy Bypass -File \"{scriptPath}\"";
                    break;
                case ".bat":
                case ".cmd":
                    fileName = "cmd.exe";
                    arguments = $"/c \"{scriptPath}\"";
                    break;
                default:
                    return new HookExecutionResult(hook.Name, false, $"지원하지 않는 스크립트 확장자: {ext} (.bat/.cmd/.ps1만 허용)");
            }

            // 사용자 정의 추가 인수
            if (!string.IsNullOrWhiteSpace(hook.Arguments))
                arguments += $" {hook.Arguments}";

            var psi = new ProcessStartInfo
            {
                FileName = fileName,
                Arguments = arguments,
                WorkingDirectory = workFolder ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
            };

            // 환경 변수로 도구 정보 전달
            psi.EnvironmentVariables["AX_TOOL_NAME"] = toolName;
            psi.EnvironmentVariables["AX_TOOL_TIMING"] = timing;
            psi.EnvironmentVariables["AX_TOOL_INPUT"] = Truncate(toolInput, MaxEnvValueLength);
            psi.EnvironmentVariables["AX_WORK_FOLDER"] = workFolder ?? "";

            if (string.Equals(timing, "post", StringComparison.OrdinalIgnoreCase))
            {
                psi.EnvironmentVariables["AX_TOOL_OUTPUT"] = Truncate(toolOutput, MaxEnvValueLength);
                psi.EnvironmentVariables["AX_TOOL_SUCCESS"] = success ? "true" : "false";
            }

            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();

            using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
            cts.CancelAfter(timeoutMs);

            try
            {
                await process.WaitForExitAsync(cts.Token);
            }
            catch (OperationCanceledException)
            {
                try { process.Kill(true); } catch (Exception) { }
                return new HookExecutionResult(hook.Name, false, $"타임아웃 ({timeoutMs}ms 초과)");
            }

            var exitCode = process.ExitCode;
            var output = stdOut.ToString().TrimEnd();
            var error = stdErr.ToString().TrimEnd();

            if (exitCode != 0)
                return new HookExecutionResult(hook.Name, false, $"종료 코드 {exitCode}: {(string.IsNullOrEmpty(error) ? output : error)}");

            return new HookExecutionResult(hook.Name, true, string.IsNullOrEmpty(output) ? "(정상 완료)" : output);
        }
        catch (Exception ex)
        {
            return new HookExecutionResult(hook.Name, false, $"훅 실행 예외: {ex.Message}");
        }
    }

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

/// <summary>훅 실행 결과.</summary>
public record HookExecutionResult(string HookName, bool Success, string Output);
