using System;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using ClaudeDo.Ui.Services;
using Iciclecreek.Terminal;

namespace ClaudeDo.Ui.ViewModels;

/// <summary>
/// Hosts a <see cref="PtyTerminalSession"/> for an embedded ConPTY terminal. The view attaches
/// its <see cref="TerminalControl"/> once loaded via <see cref="AttachControl"/>; <see cref="Start"/>
/// can be called before or after attach — whichever happens second triggers the launch.
/// </summary>
public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDisposable
{
    // A process that dies within this window of becoming "running" is treated as a startup
    // failure (see OnSessionProcessExited) rather than a normal session end, even though the
    // spawn itself succeeded — e.g. `claude --session-id <guid>` exiting immediately on an
    // auth/network hiccup. Long enough to clear normal CLI startup, short enough not to
    // mistake a real crash-after-use for a start failure.
    private static readonly TimeSpan StartupGraceWindow = TimeSpan.FromSeconds(5);

    private readonly PtyTerminalSession _session = new();
    private readonly Func<DateTime> _utcNow;
    private TerminalControl? _control;
    private TerminalLaunchDescriptor? _pendingDescriptor;
    private DateTime? _startedAtUtc;

    [ObservableProperty] private bool _isRunning;
    [ObservableProperty] private bool _hasExited;
    [ObservableProperty] private int? _exitCode;
    [ObservableProperty] private string? _startError;

    /// <summary>True from construction until the child process is actually launched (or the launch
    /// failed) — covers both the caller's launch-spec roundtrip and the ConPTY spawn, so the host
    /// can show a spinner instead of an empty black pane.</summary>
    public bool IsStarting => !IsRunning && !HasExited && StartError is null;

    partial void OnIsRunningChanged(bool value)
    {
        OnPropertyChanged(nameof(IsStarting));
        if (value) _startedAtUtc = _utcNow();
    }

    partial void OnHasExitedChanged(bool value)   => OnPropertyChanged(nameof(IsStarting));
    partial void OnStartErrorChanged(string? value) => OnPropertyChanged(nameof(IsStarting));

    public InteractiveTerminalViewModel() : this(() => DateTime.UtcNow)
    {
    }

    /// <summary>Test-only seam for controlling "time since launch" without real delays.</summary>
    internal InteractiveTerminalViewModel(Func<DateTime> utcNow)
    {
        _utcNow = utcNow;
        _session.ProcessExited += OnSessionProcessExited;
    }

    /// <summary>Called by the view once its <see cref="TerminalControl"/> has loaded (template applied).</summary>
    public void AttachControl(TerminalControl control)
    {
        _control = control;
        if (_pendingDescriptor is { } descriptor)
        {
            _pendingDescriptor = null;
            _ = StartCoreAsync(descriptor);
        }
    }

    public void Start(TerminalLaunchDescriptor descriptor)
    {
        if (_control is null)
        {
            _pendingDescriptor = descriptor;
            return;
        }
        _ = StartCoreAsync(descriptor);
    }

    private async Task StartCoreAsync(TerminalLaunchDescriptor descriptor)
    {
        if (_control is null) return;
        try
        {
            StartError = null;
            await _session.StartAsync(descriptor, _control);
            IsRunning = true;
            HasExited = false;
            ExitCode = null;
        }
        catch (Exception ex)
        {
            IsRunning = false;
            HasExited = true;
            StartError = ex.Message;
        }
    }

    /// <summary>Internal (not private) so tests can drive it directly without a real ConPTY spawn.</summary>
    internal void OnSessionProcessExited(object? sender, int exitCode)
    {
        IsRunning = false;
        HasExited = true;
        ExitCode = exitCode;

        // Died at startup: route through the same banner as a launch-time failure instead of
        // leaving a dead terminal with no Retry affordance (ConPtyPaneViewModel.CanRetry
        // requires StartError).
        if (exitCode != 0 && _startedAtUtc is { } startedAt && _utcNow() - startedAt < StartupGraceWindow)
            StartError = $"Session exited immediately (code {exitCode}).";
    }

    /// <summary>Reports a failure that happened before <see cref="Start"/> could be called (e.g. the
    /// launch-spec roundtrip threw), so it surfaces through the same banner as a spawn failure.</summary>
    public void ReportStartFailure(string message)
    {
        IsRunning = false;
        HasExited = true;
        StartError = message;
    }

    public void Kill() => _session.Kill();

    public void Dispose()
    {
        _session.ProcessExited -= OnSessionProcessExited;
        _session.Dispose();
    }
}
