using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.ViewModels.MissionControl;
using Microsoft.EntityFrameworkCore;

namespace ClaudeDo.Ui.ViewModels;

public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
{
    private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
    private readonly IWorkerClient _worker;
    private readonly Action<string, string, DateTime> _onTaskStarted;
    private readonly Action<string, string, string, DateTime> _onTaskFinished;
    private readonly Action<string> _onTaskUpdated;
    private readonly Action _onConnectionRestored;

    public ObservableCollection<TaskMonitorViewModel> Monitors { get; } = new();

    // Embedded ConPTY sessions (task-based only) — a manual cockpit detached from the
    // review/merge/status machinery. Mirrored into Panes alongside the streamed-log Monitors.
    public ObservableCollection<ConPtyPaneViewModel> ConPtySessions { get; } = new();

    // Unified view of Monitors ++ ConPtySessions (in that order) so the layout toggle can
    // present one heterogeneous collection as either a grid or tabs.
    public ObservableCollection<IMissionControlPane> Panes { get; } = new();

    [ObservableProperty] private int _columnCount = 1;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(LayoutToggleLabel))]
    private bool _isFocusMode;

    [ObservableProperty] private IMissionControlPane? _focusedPane;

    public string LayoutToggleLabel => Loc.T(IsFocusMode ? "missionControl.overviewMode" : "missionControl.focusMode");

    /// <summary>Surfaces a Command Center failure (e.g. a ConPTY launch spec fetch) — the shell
    /// wires this into the footer error strip, same as the island view models' ErrorReported.</summary>
    public event Action<string>? ErrorReported;

    private Action<string>? _openInApp;
    public Action<string>? OpenInApp
    {
        get => _openInApp;
        set
        {
            _openInApp = value;
            foreach (var m in Monitors) m.OpenInAppRequested = value;
        }
    }

    // View-layer seam: show a detached monitor in its own window. Second arg is the re-dock callback
    // invoked when that window closes.
    public Action<TaskMonitorViewModel, Action>? ShowDetached { get; set; }

    // View-layer seam: open the app Settings modal from the Mission Control window.
    public Action? OpenSettingsRequested { get; set; }

    public bool HasMonitors => Monitors.Count > 0;
    public bool HasPanes => Panes.Count > 0;

    // Read-only view of the worker queue (tasks waiting to run), shown as a side strip.
    public ObservableCollection<QueuedTaskViewModel> Queued { get; } = new();
    public bool HasQueued => Queued.Count > 0;

    public MissionControlViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker)
    {
        _dbFactory = dbFactory;
        _worker = worker;

        Monitors.CollectionChanged += OnMonitorsChanged;
        ConPtySessions.CollectionChanged += OnConPtySessionsChanged;
        Panes.CollectionChanged += OnPanesChanged;

        _onTaskStarted = (slot, taskId, startedAt) => { EnsureMonitor(taskId); _ = RefreshQueueAsync(); };
        _worker.TaskStartedEvent += _onTaskStarted;

        _onTaskFinished = (slot, taskId, status, finishedAt) => _ = RefreshQueueAsync();
        _worker.TaskFinishedEvent += _onTaskFinished;

        _onTaskUpdated = taskId => _ = RefreshQueueAsync();
        _worker.TaskUpdatedEvent += _onTaskUpdated;

        _onConnectionRestored = () => { SeedActive(); _ = RefreshQueueAsync(); };
        _worker.ConnectionRestoredEvent += _onConnectionRestored;

        SeedActive();
        _ = RefreshQueueAsync();
    }

    internal async System.Threading.Tasks.Task RefreshQueueAsync()
    {
        try
        {
            await using var ctx = await _dbFactory.CreateDbContextAsync();
            var rows = await ctx.Tasks.AsNoTracking()
                .Where(t => t.Status == ClaudeDo.Data.Models.TaskStatus.Queued)
                .OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
                .Select(t => new { t.Id, t.Title, t.BlockedByTaskId })
                .ToListAsync();

            Queued.Clear();
            foreach (var r in rows)
                Queued.Add(new QueuedTaskViewModel
                {
                    Id = r.Id,
                    Title = r.Title ?? string.Empty,
                    IsBlocked = r.BlockedByTaskId != null,
                });
            OnPropertyChanged(nameof(HasQueued));
        }
        catch { /* best-effort queue refresh */ }
    }

    // Drop-to-queue: a task dragged from the main app onto Mission Control gets queued.
    public async System.Threading.Tasks.Task EnqueueTaskAsync(string taskId)
    {
        if (string.IsNullOrEmpty(taskId)) return;
        try
        {
            await using var db = await _dbFactory.CreateDbContextAsync();
            var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == taskId);
            if (entity is null
                || entity.Status == ClaudeDo.Data.Models.TaskStatus.Running
                || entity.Status == ClaudeDo.Data.Models.TaskStatus.Queued)
                return;
            entity.Status = ClaudeDo.Data.Models.TaskStatus.Queued;
            await db.SaveChangesAsync();
            await _worker.WakeQueueAsync();
        }
        catch { /* best-effort enqueue */ }
        await RefreshQueueAsync();
    }

    private void SeedActive()
    {
        foreach (var a in _worker.GetActiveTasks())
            EnsureMonitor(a.TaskId);
    }

    private void EnsureMonitor(string taskId)
    {
        if (string.IsNullOrEmpty(taskId)) return;
        if (Monitors.Any(m => m.SubscribedTaskId == taskId)) return;

        var monitor = new TaskMonitorViewModel(_dbFactory, _worker);
        monitor.SetTaskId(taskId);
        monitor.OpenInAppRequested = _openInApp;
        monitor.DetachRequested = Detach;
        Monitors.Add(monitor);
        _ = HydrateAsync(monitor, taskId);
    }

    private void Detach(TaskMonitorViewModel monitor)
    {
        if (!Monitors.Contains(monitor)) return;
        monitor.IsDetached = true;
        Monitors.Remove(monitor);            // drop from grid — do NOT dispose; it keeps streaming
        ShowDetached?.Invoke(monitor, () => ReDock(monitor));
    }

    private void ReDock(TaskMonitorViewModel monitor)
    {
        monitor.IsDetached = false;
        if (!Monitors.Contains(monitor) && monitor.SubscribedTaskId is not null)
            Monitors.Add(monitor);           // back into the grid
    }

    private async System.Threading.Tasks.Task HydrateAsync(TaskMonitorViewModel monitor, string taskId)
    {
        try
        {
            await using var ctx = await _dbFactory.CreateDbContextAsync();
            var entity = await ctx.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId);
            if (entity is null || monitor.SubscribedTaskId != taskId) return;
            monitor.ApplyState(entity.Status);
            monitor.Title = entity.Title;
            var latestRun = await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId);
            monitor.ApplyOutcome(entity.Result, latestRun?.ErrorMarkdown);
            await monitor.ReplayLogFileAsync(entity.LogPath, CancellationToken.None);

            // Re-attach: if the task is blocked on an AskUser question right now, surface it.
            var pending = await _worker.GetPendingQuestionAsync(taskId);
            if (pending is not null && monitor.SubscribedTaskId == taskId)
                monitor.SetPendingQuestion(pending.QuestionId, pending.Question);
        }
        catch { /* best-effort hydrate */ }
    }

    [RelayCommand]
    private void ClearFinished()
    {
        foreach (var m in Monitors.Where(m => m.IsDone || m.IsFailed || m.IsCancelled || m.IsWaitingForReview).ToList())
        {
            Monitors.Remove(m);
            m.Dispose();
        }
    }

    [RelayCommand]
    private void OpenSettings() => OpenSettingsRequested?.Invoke();

    [RelayCommand]
    private void ToggleLayout() => IsFocusMode = !IsFocusMode;

    public void MoveMonitor(TaskMonitorViewModel dragged, TaskMonitorViewModel target)
    {
        if (ReferenceEquals(dragged, target)) return;
        var from = Monitors.IndexOf(dragged);
        var to = Monitors.IndexOf(target);
        if (from < 0 || to < 0) return;
        Monitors.Move(from, to);
    }

    // Fetches the launch spec for a task's worktree and hosts an embedded ConPTY session as a
    // Command Center pane (task-based only).
    public async System.Threading.Tasks.Task OpenConPtySessionAsync(string taskId)
    {
        if (string.IsNullOrEmpty(taskId)) return;
        if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is { } existing)
        {
            FocusedPane = existing;
            return;
        }

        var title = taskId;
        try
        {
            await using var ctx = await _dbFactory.CreateDbContextAsync();
            var entity = await ctx.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId);
            if (entity?.Title is { Length: > 0 } t) title = t;
        }
        catch { /* best-effort title lookup */ }

        try
        {
            var spec = await _worker.GetInteractiveLaunchSpecAsync(taskId);
            var descriptor = new TerminalLaunchDescriptor(spec.Cwd, spec.Exe, spec.Args, spec.Env);
            var pane = new ConPtyPaneViewModel(taskId, title, descriptor);
            pane.ErrorReported += OnConPtyPaneError;
            pane.CloseRequested += CloseConPtySession;
            ConPtySessions.Add(pane);
        }
        catch (Exception ex)
        {
            ErrorReported?.Invoke(Loc.T("missionControl.conptyLaunchFailed", ex.Message));
        }
    }

    // Ad-hoc (task-less) ConPTY session in a user-chosen directory. Never deduped — every call
    // opens a fresh pane, unlike the task-based OpenConPtySessionAsync above.
    public async System.Threading.Tasks.Task OpenAdHocConPtySessionAsync(string directory)
    {
        if (string.IsNullOrEmpty(directory)) return;

        var title = Path.GetFileName(directory.TrimEnd('\\', '/'));
        if (string.IsNullOrEmpty(title)) title = directory;

        try
        {
            var spec = await _worker.GetAdHocLaunchSpecAsync(directory);
            var descriptor = new TerminalLaunchDescriptor(spec.Cwd, spec.Exe, spec.Args, spec.Env);
            var pane = ConPtyPaneViewModel.CreateAdHoc(title, descriptor);
            pane.ErrorReported += OnConPtyPaneError;
            pane.CloseRequested += CloseConPtySession;
            ConPtySessions.Add(pane);
        }
        catch (Exception ex)
        {
            ErrorReported?.Invoke(Loc.T("missionControl.conptyLaunchFailed", ex.Message));
        }
    }

    private void OnConPtyPaneError(string message) => ErrorReported?.Invoke(message);

    private void CloseConPtySession(ConPtyPaneViewModel pane)
    {
        if (!ConPtySessions.Contains(pane)) return;
        pane.ErrorReported -= OnConPtyPaneError;
        pane.CloseRequested -= CloseConPtySession;
        ConPtySessions.Remove(pane);
        pane.Dispose();
    }

    // Mirrors Monitors' add/remove/move into the front (Monitors-prefix) section of Panes.
    private void OnMonitorsChanged(object? sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                Panes.Insert(e.NewStartingIndex, (TaskMonitorViewModel)e.NewItems![0]!);
                break;
            case NotifyCollectionChangedAction.Remove:
                Panes.RemoveAt(e.OldStartingIndex);
                break;
            case NotifyCollectionChangedAction.Move:
                Panes.Move(e.OldStartingIndex, e.NewStartingIndex);
                break;
            default: // Reset (Dispose's Monitors.Clear())
                foreach (var p in Panes.OfType<TaskMonitorViewModel>().ToList())
                    Panes.Remove(p);
                break;
        }
        OnPropertyChanged(nameof(HasMonitors));
    }

    // Mirrors ConPtySessions' add/remove into the tail (ConPtySessions-suffix) section of Panes.
    private void OnConPtySessionsChanged(object? sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                Panes.Insert(Monitors.Count + e.NewStartingIndex, (ConPtyPaneViewModel)e.NewItems![0]!);
                break;
            case NotifyCollectionChangedAction.Remove:
                Panes.RemoveAt(Monitors.Count + e.OldStartingIndex);
                break;
            default: // Reset
                foreach (var p in Panes.OfType<ConPtyPaneViewModel>().ToList())
                    Panes.Remove(p);
                break;
        }
    }

    private void OnPanesChanged(object? sender, NotifyCollectionChangedEventArgs e)
    {
        ColumnCount = Panes.Count switch
        {
            <= 1 => 1,
            <= 4 => 2,
            _    => 3,
        };
        OnPropertyChanged(nameof(HasPanes));
        if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems?[0] is IMissionControlPane added)
            FocusedPane = added;
        else if (e.Action == NotifyCollectionChangedAction.Remove && ReferenceEquals(FocusedPane, e.OldItems?[0]))
            FocusedPane = Panes.LastOrDefault();
    }

    public void Dispose()
    {
        _worker.TaskStartedEvent -= _onTaskStarted;
        _worker.TaskFinishedEvent -= _onTaskFinished;
        _worker.TaskUpdatedEvent -= _onTaskUpdated;
        _worker.ConnectionRestoredEvent -= _onConnectionRestored;
        Monitors.CollectionChanged -= OnMonitorsChanged;
        ConPtySessions.CollectionChanged -= OnConPtySessionsChanged;
        Panes.CollectionChanged -= OnPanesChanged;
        foreach (var m in Monitors) m.Dispose();
        Monitors.Clear();
        foreach (var c in ConPtySessions.ToList())
        {
            c.ErrorReported -= OnConPtyPaneError;
            c.CloseRequested -= CloseConPtySession;
            c.Dispose();
        }
        ConPtySessions.Clear();
        Panes.Clear();
    }
}

/// <summary>Read-only display row for a queued task in the Mission Control side strip.</summary>
public sealed class QueuedTaskViewModel
{
    public required string Id { get; init; }
    public required string Title { get; init; }
    public bool IsBlocked { get; init; }
}
