using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Iciclecreek.Terminal;

namespace ClaudeDo.Ui.Services;

/// <summary>
/// Thin wrapper around <see cref="TerminalControl.LaunchProcess()"/> — the library owns the
/// Porta.Pty spawn, keyboard input, rendering, resize, and focus end to end (see
/// <c>spikes/ConPtyTerminal/MainWindow.axaml.cs</c>, which proved this renders correctly and
/// stays responsive). We only:
/// 1) apply <see cref="TerminalLaunchDescriptor.Env"/> onto the current process environment
///    before launching — Porta.Pty inherits the calling process's environment and there is no
///    per-launch env seam on <see cref="TerminalControl"/>/<see cref="TerminalControl.LaunchProcess()"/> —
/// 2) relay the control's own <see cref="TerminalControl.ProcessExited"/> event and
///    <see cref="TerminalControl.Kill()"/> method.
/// </summary>
public sealed class PtyTerminalSession : IDisposable
{
    // Guards the set-env + LaunchProcess critical section below: two sessions starting
    // back-to-back (e.g. planning sessions for two different tasks) could otherwise interleave
    // their SetEnvironmentVariable calls before either LaunchProcess() forks, so one process
    // inherits the other's env (e.g. CLAUDEDO_PLANNING_TOKEN, breaking that session's own MCP
    // auth). Process-wide env leakage AFTER a launch has forked remains a documented limitation
    // — Porta.Pty has no per-launch env seam, so the vars stay set on the whole UI process.
    // The wait is bounded (see WaitForLaunchGateAsync) — a hung launch (slow disk, AV scanning
    // claude.exe, a Porta.Pty/ConPTY hiccup) must not freeze every other pane open behind it.
    private static readonly SemaphoreSlim s_launchGate = new(1, 1);
    private static readonly TimeSpan s_launchGateTimeout = TimeSpan.FromSeconds(30);

    /// <summary>
    /// Waits on <paramref name="gate"/> for at most <paramref name="timeout"/>, throwing
    /// <see cref="TimeoutException"/> instead of blocking forever. Never acquires the gate on
    /// timeout, so callers must not release it in that case.
    /// </summary>
    internal static async Task WaitForLaunchGateAsync(SemaphoreSlim gate, TimeSpan timeout, CancellationToken ct)
    {
        if (!await gate.WaitAsync(timeout, ct))
            throw new TimeoutException("Another terminal launch is still starting up. Please retry in a moment.");
    }

    private TerminalControl? _control;
    private bool _disposed;

    public bool IsRunning { get; private set; }
    public int? ExitCode { get; private set; }

    /// <summary>Raised when the child process exits, on the UI thread, with its exit code.</summary>
    public event EventHandler<int>? ProcessExited;

    /// <summary>
    /// Applies <paramref name="descriptor"/>'s env vars to the current process, then drives
    /// <paramref name="control"/> to launch it via <see cref="TerminalControl.LaunchProcess()"/>.
    /// </summary>
    public async Task StartAsync(TerminalLaunchDescriptor descriptor, TerminalControl control, CancellationToken ct = default)
    {
        if (_control is not null) throw new InvalidOperationException("Session already started.");
        _control = control;
        control.ProcessExited += OnControlProcessExited;

        control.Process = descriptor.Exe;
        control.Args = new List<string>(descriptor.Args);
        control.StartingDirectory = descriptor.Cwd;

        await WaitForLaunchGateAsync(s_launchGate, s_launchGateTimeout, ct);
        try
        {
            foreach (var (key, value) in descriptor.Env)
                Environment.SetEnvironmentVariable(key, value);

            await control.LaunchProcess();
        }
        finally
        {
            s_launchGate.Release();
        }

        // Permanent reparent mode: TerminalView.OnDetachedFromLogicalTree kills the child
        // process unless BeginReparent() suppressed it, and Mission Control detaches pane
        // views routinely (overview-grid rebuilds on pane add/remove, focus-mode tab
        // switches). ClaudeDo owns teardown explicitly instead — ConPtyPaneViewModel.Dispose
        // calls Kill() when a pane closes — so EndReparent is deliberately never called.
        control.BeginReparent();
        IsRunning = true;
    }

    private void OnControlProcessExited(object? sender, ProcessExitedEventArgs e)
    {
        IsRunning = false;
        ExitCode = e.ExitCode;
        ProcessExited?.Invoke(this, e.ExitCode);
    }

    public void Kill()
    {
        try { _control?.Kill(); }
        catch (Exception) { /* already exited */ }
    }

    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;

        if (_control is not null)
            _control.ProcessExited -= OnControlProcessExited;
    }
}
