using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace SquadDash;

internal enum LoopStopState { None, StopRequested, Aborted }

/// <summary>
/// Drives the Native-Loop-in-Bridge feature.
/// Runs iterations on a background Task; all WPF UI callbacks are marshalled
/// by the caller (MainWindow) via Dispatcher before being passed as delegates.
/// </summary>
internal sealed class LoopController {

    private readonly Func<string, string?, Task> _executePromptAsync;
    private readonly Action                      _abortPrompt;
    private readonly Action<int>                 _onIterationStarted;
    private readonly Action                      _onStopped;
    private readonly Action<string>              _onError;
    private readonly Action<int>                 _onIterationCompleted;
    private readonly Action<DateTimeOffset>      _onWaiting;
    private readonly Func<Task>                  _onBeforeIteration;
    private readonly Func<Task>                  _onBeforeWait;

    private volatile bool         _stopRequested;
    private CancellationTokenSource? _cts;
    private string? _workspacePath;
    private string? _filterText;

    internal bool          IsRunning  { get; private set; }
    internal LoopStopState StopState  { get; private set; }

    /// <param name="executePromptAsync">
    ///   <c>(prompt, sessionId) =&gt; …</c> — second arg is an optional session-id
    ///   override (null = use the active conversation session).
    ///   Must be dispatched to the UI thread by MainWindow before passing.
    /// </param>
    /// <param name="abortPrompt">Calls <c>_bridge.AbortPrompt()</c>.</param>
    /// <param name="onIterationStarted">Fired with the 1-based iteration number.</param>
    /// <param name="onStopped">Fired when the loop exits normally or via RequestStop.</param>
    /// <param name="onError">Fired with a human-readable message on timeout or abort.</param>
    /// <param name="onIterationCompleted">Fired with the 1-based iteration number after success.</param>
    /// <param name="onWaiting">Fired with the target start time of the next iteration when the loop enters the inter-iteration delay.</param>
    /// <param name="onBeforeIteration">Awaited at the top of each cycle, before the loop prompt fires. Use this to drain a prompt queue before each iteration.</param>
    /// <param name="onBeforeWait">Awaited after each iteration completes, before the inter-iteration delay. Use this to drain a prompt queue after each iteration.</param>
    internal LoopController(
        Func<string, string?, Task> executePromptAsync,
        Action                      abortPrompt,
        Action<int>                 onIterationStarted,
        Action                      onStopped,
        Action<string>              onError,
        Action<int>                 onIterationCompleted,
        Action<DateTimeOffset>      onWaiting,
        Func<Task>?                 onBeforeIteration = null,
        Func<Task>?                 onBeforeWait      = null) {

        _executePromptAsync   = executePromptAsync;
        _abortPrompt          = abortPrompt;
        _onIterationStarted   = onIterationStarted;
        _onStopped            = onStopped;
        _onError              = onError;
        _onIterationCompleted = onIterationCompleted;
        _onWaiting            = onWaiting;
        _onBeforeIteration    = onBeforeIteration ?? (() => Task.CompletedTask);
        _onBeforeWait         = onBeforeWait      ?? (() => Task.CompletedTask);
    }

    /// <summary>
    /// Starts the loop on a background Task and returns immediately.
    /// Does nothing if already running.
    /// </summary>
    /// <param name="resumeFromIteration">
    /// When resuming after an auto-pause (e.g. queue interrupt), pass the last
    /// completed iteration number so the counter continues rather than restarting at 1.
    /// </param>
    internal Task StartAsync(LoopMdConfig config, bool continuousContext, string? workspacePath = null, int resumeFromIteration = 0, string? filterText = null) {
        if (IsRunning)
            return Task.CompletedTask;

        _stopRequested = false;
        _workspacePath = workspacePath;
        _filterText    = filterText;
        _cts           = new CancellationTokenSource();
        // Fire-and-forget; the loop reports completion via callbacks.
        _ = Task.Run(() => RunLoopAsync(config, continuousContext, _cts.Token, resumeFromIteration));
        return Task.CompletedTask;
    }

    /// <summary>Graceful stop: finishes the current iteration then halts.</summary>
    internal void RequestStop() {
        _stopRequested = true;
        if (StopState == LoopStopState.None)
            StopState = LoopStopState.StopRequested;
    }

    /// <summary>Immediate abort: calls <c>abortPrompt</c> and cancels the loop CTS.</summary>
    internal void RequestAbort() {
        StopState      = LoopStopState.Aborted;
        _stopRequested = true;
        _abortPrompt();
        _cts?.Cancel();
    }

    private async Task RunLoopAsync(
        LoopMdConfig      config,
        bool              continuousContext,
        CancellationToken ct,
        int               resumeFromIteration = 0) {

        IsRunning = true;
        StopState = LoopStopState.None;
        int iteration = resumeFromIteration;

        try {
            while (!_stopRequested && !ct.IsCancellationRequested) {
                // Drain any queued prompts before firing this iteration.
                // A stray exception from the hook (e.g. an aborted queued item whose
                // cancellation escapes the delegate) must not kill the loop.
                try {
                    await _onBeforeIteration();
                } catch (Exception) {
                    if (_stopRequested || ct.IsCancellationRequested) break;
                    continue;
                }
                if (_stopRequested) break;

                iteration++;
                _onIterationStarted(iteration);

                // continuousContext=false → each iteration gets its own session so
                // agent state does not accumulate across rounds.
                var sessionId = continuousContext ? null : Guid.NewGuid().ToString("N");

                var expandedInstructions = ExpandVariables(config.Instructions, config, iteration, _workspacePath, _filterText);
                var prompt     = BuildAugmentedPrompt(expandedInstructions, config.Commands);
                var promptTask = _executePromptAsync(prompt, sessionId);

                // Race the prompt against the per-iteration wall-clock timeout.
                // Task.Delay with a dedicated CTS fires independently of the outer ct,
                // so we can distinguish a true timeout from a user-requested abort.
                using var timeoutCts = new CancellationTokenSource(
                    TimeSpan.FromMinutes(config.TimeoutMinutes));
                var timeoutExpired = Task.Delay(Timeout.InfiniteTimeSpan, timeoutCts.Token);

                if (await Task.WhenAny(promptTask, timeoutExpired) != promptTask) {
                    // Timeout fired before the prompt finished — abort it, then report.
                    _abortPrompt();
                    try { await promptTask; } catch { /* swallow post-abort exception */ }
                    _onError(
                        $"Iteration {iteration} timed out after {config.TimeoutMinutes} min");
                    break;
                }

                await promptTask; // propagate any exception thrown by the prompt itself

                _onIterationCompleted(iteration);

                // Check max_iterations (0 = unlimited)
                if (config.Options != null) {
                    var maxOpt = config.Options.FirstOrDefault(o => o.Key == "max_iterations");
                    if (maxOpt != null &&
                        int.TryParse(maxOpt.RawValue, out var maxIter) &&
                        maxIter > 0 &&
                        iteration >= maxIter) {
                        _stopRequested = true;
                    }
                }

                if (_stopRequested) break;

                await _onBeforeWait();
                if (_stopRequested) break;

                var nextAt = DateTimeOffset.Now + TimeSpan.FromMinutes(config.IntervalMinutes);
                _onWaiting(nextAt);
                await Task.Delay(TimeSpan.FromMinutes(config.IntervalMinutes), ct);
            }
        }
        finally {
            IsRunning = false;
            if (StopState == LoopStopState.Aborted)
                _onError("Loop aborted.");
            else
                _onStopped();
        }
    }

    /// <summary>
    /// Appends a HOST_COMMAND_JSON reference block to the loop instructions if the
    /// loop.md frontmatter declared any <c>commands</c>. AI invokes commands by
    /// appending a HOST_COMMAND_JSON block at the very end of its response.
    /// </summary>
    internal static string BuildAugmentedPrompt(string instructions, IReadOnlyList<string>? commands) {
        if (commands is null || commands.Count == 0)
            return instructions;

        var sb = new System.Text.StringBuilder(instructions);
        sb.AppendLine();
        sb.AppendLine();
        sb.AppendLine("---");
        sb.AppendLine("## SquadDash loop commands (available this iteration)");
        sb.AppendLine();
        sb.AppendLine("To invoke a SquadDash command, append a HOST_COMMAND_JSON block at the **very end** of your response (after all other content including <system_notification>):");
        sb.AppendLine();
        sb.AppendLine("```");
        sb.AppendLine("HOST_COMMAND_JSON:");
        sb.AppendLine("[");
        sb.AppendLine("  { \"command\": \"command_name\" }");
        sb.AppendLine("]");
        sb.AppendLine("```");
        sb.AppendLine();
        sb.AppendLine("Available commands this iteration:");
        sb.AppendLine();

        foreach (var cmd in commands) {
            switch (cmd.Trim().ToLowerInvariant()) {
                case "stop_loop":
                    sb.AppendLine("- **stop_loop** — Stops the loop after this iteration completes.");
                    break;
                case "start_loop":
                    sb.AppendLine("- **start_loop** — Starts (or restarts) the SquadDash native loop.");
                    break;
                default:
                    sb.AppendLine($"- **{cmd}**");
                    break;
            }
        }

        sb.AppendLine();
        sb.AppendLine("Only emit HOST_COMMAND_JSON when the condition for that command has been met. Do not emit it on every iteration.");
        return sb.ToString().TrimEnd();
    }

    private static string ExpandVariables(string text, LoopMdConfig config, int iteration, string? workspacePath, string? filterText = null) {
        // Conditional blocks must be evaluated before plain substitution so that {{key}}
        // tokens inside included blocks are resolved in the pass below.
        text = LoopMdParser.PreprocessConditionals(text, config.Options);

        // Option variables — replace {{key}} with the raw value from the options block.
        // Group-type options are UI-only headers with no substitutable value; skip them.
        if (config.Options != null)
            foreach (var opt in config.Options)
            {
                if (opt.Type == "group") continue;
                text = text.Replace($"{{{{{opt.Key}}}}}", opt.RawValue, StringComparison.Ordinal);
            }

        // System variables
        text = text.Replace("{{iteration}}", iteration.ToString(System.Globalization.CultureInfo.InvariantCulture), StringComparison.Ordinal);
        text = text.Replace("{{copilot_trailer}}", "Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>", StringComparison.Ordinal);
        text = text.Replace("{{workspace_path}}", workspacePath ?? string.Empty, StringComparison.Ordinal);
        text = text.Replace("{{build_command}}", DetectBuildCommand(workspacePath), StringComparison.Ordinal);

        // [**FILTER**] — inject the task-filter instruction (bracket syntax, not {{...}}).
        text = text.Replace("[**FILTER**]", LoopMdParser.BuildFilterInstruction(filterText), StringComparison.Ordinal);

        return text;
    }

    private static string DetectBuildCommand(string? workspacePath) {
        if (workspacePath == null || !Directory.Exists(workspacePath))
            return string.Empty;
        if (Directory.GetFiles(workspacePath, "*.sln").Length > 0 ||
            Directory.GetFiles(workspacePath, "*.csproj").Length > 0)
            return "dotnet build";
        if (File.Exists(Path.Combine(workspacePath, "package.json")))
            return "npm run build";
        if (File.Exists(Path.Combine(workspacePath, "Cargo.toml")))
            return "cargo build";
        if (File.Exists(Path.Combine(workspacePath, "go.mod")))
            return "go build ./...";
        if (File.Exists(Path.Combine(workspacePath, "Makefile")))
            return "make";
        return string.Empty;
    }
}
