using System.Collections.ObjectModel;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input.Platform;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Ui.ViewModels.Modals;

public enum BatchMergeOutcome { None, Merging, Merged, Conflict, Blocked, VerifyFailed, Failed }

public sealed partial class WorktreeOverviewRowViewModel : ViewModelBase
{
    [ObservableProperty] private string _taskId = "";
    [ObservableProperty] private string _taskTitle = "";
    [ObservableProperty][NotifyPropertyChangedFor(nameof(IsRunning))] private TaskStatus _taskStatus;
    [ObservableProperty] private string _listId = "";
    [ObservableProperty] private string _listName = "";
    [ObservableProperty] private string _path = "";
    [ObservableProperty] private string _branchName = "";
    [ObservableProperty] private string _baseCommit = "";
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(IsActive))]
    [NotifyPropertyChangedFor(nameof(IsMerged))]
    [NotifyPropertyChangedFor(nameof(IsDiscarded))]
    [NotifyPropertyChangedFor(nameof(IsKept))]
    private WorktreeState _state;
    [ObservableProperty] private string? _diffStat;
    [ObservableProperty][NotifyPropertyChangedFor(nameof(AgeText))] private DateTime _createdAt;
    [ObservableProperty] private bool _pathExistsOnDisk;
    [ObservableProperty] private bool _isSelected;
    [ObservableProperty] private bool _isChecked;
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(IsConflict))]
    [NotifyPropertyChangedFor(nameof(HasOutcome))]
    private BatchMergeOutcome _mergeOutcome;

    public bool IsConflict => MergeOutcome == BatchMergeOutcome.Conflict;
    public bool HasOutcome => MergeOutcome != BatchMergeOutcome.None;

    public string AgeText => FormatAge(DateTime.UtcNow - CreatedAt);
    public bool IsActive => State == WorktreeState.Active;
    public bool IsMerged => State == WorktreeState.Merged;
    public bool IsDiscarded => State == WorktreeState.Discarded;
    public bool IsKept => State == WorktreeState.Kept;
    public bool IsRunning => TaskStatus == TaskStatus.Running;

    private static string FormatAge(TimeSpan ts)
    {
        if (ts.TotalDays >= 1) return $"{(int)ts.TotalDays}d ago";
        if (ts.TotalHours >= 1) return $"{(int)ts.TotalHours}h ago";
        if (ts.TotalMinutes >= 1) return $"{(int)ts.TotalMinutes}m ago";
        return "just now";
    }
}

public sealed partial class WorktreesGroupViewModel : ViewModelBase
{
    public required string ListId { get; init; }
    public required string ListName { get; init; }
    public ObservableCollection<WorktreeOverviewRowViewModel> Rows { get; } = new();
}

public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
{
    private readonly IWorkerClient _worker;
    private readonly Func<DiffViewerViewModel> _diffVmFactory;
    private readonly MergeCoordinator _merge;

    [ObservableProperty] private string? _listIdFilter;
    [ObservableProperty] private string _title = "Worktrees";
    [ObservableProperty] private bool _isGlobal;
    [ObservableProperty] private string? _statusMessage;
    [ObservableProperty] private WorktreeOverviewRowViewModel? _selectedRow;
    [ObservableProperty][NotifyCanExecuteChangedFor(nameof(MergeAllCommand))] private string? _selectedTarget;
    [ObservableProperty][NotifyCanExecuteChangedFor(nameof(MergeAllCommand))] private int _selectedCount;
    [ObservableProperty] private string? _batchProgress;

    // Separate per-action status: a running Refresh must not block the Cleanup indicator (and
    // vice versa). OperationStatus is a nested ObservableObject, so [NotifyCanExecuteChangedFor]
    // can't observe it directly — IsRunning changes are wired to NotifyCanExecuteChanged() below.
    public OperationStatus RefreshStatus { get; } = new();
    public OperationStatus CleanupStatus { get; } = new();
    public OperationStatus ForceRemoveStatus { get; } = new();
    public OperationStatus BatchMergeStatus { get; } = new();

    public ObservableCollection<WorktreeOverviewRowViewModel> Rows { get; } = new();
    public ObservableCollection<WorktreesGroupViewModel> Groups { get; } = new();
    public ObservableCollection<string> MergeTargets { get; } = new();
    public ObservableCollection<WorktreeOverviewRowViewModel> ConflictRows { get; } = new();

    // Mirrors UsageMonitorModalViewModel.ErrorReported — surfaces modal-owned failures in the
    // footer strip once the caller wires it (the modal's own window sits in front of the strip).
    public event Action<string>? ErrorReported;

    public Action? CloseAction { get; set; }
    public Action<DiffViewerViewModel>? ShowDiffAction { get; set; }
    public Action<string, string>? JumpToTaskAction { get; set; }
    public Func<string, Task<bool>>? ConfirmAction { get; set; }
    public Func<MergeModalViewModel>? ResolveMergeVm { get; set; }
    public Func<MergeModalViewModel, Task>? ShowMergeAction { get; set; }

    public WorktreesOverviewModalViewModel(IWorkerClient worker, Func<DiffViewerViewModel> diffVmFactory, MergeCoordinator merge)
    {
        _worker = worker;
        _diffVmFactory = diffVmFactory;
        _merge = merge;

        BatchMergeStatus.PropertyChanged += (_, e) =>
        {
            if (e.PropertyName == nameof(OperationStatus.IsRunning)) MergeAllCommand.NotifyCanExecuteChanged();
        };
        ForceRemoveStatus.PropertyChanged += (_, e) =>
        {
            if (e.PropertyName == nameof(OperationStatus.IsRunning)) ForceRemoveCommand.NotifyCanExecuteChanged();
        };

        // Phase 3 reconcile tick: this overlay is long-lived (stays open while the user reviews
        // worktrees), so refresh it on the same cadence as TasksIslandViewModel's tick instead of
        // leaving it frozen at the moment it was opened. Skipped while a load or batch merge is
        // already in flight — reloading mid-merge would replace the row instances the merge loop
        // is still updating.
        _reconcileTimer.Elapsed += (_, _) =>
            Avalonia.Threading.Dispatcher.UIThread.Post(() => _ = ReconcileTickAsync());
        _reconcileTimer.Start();
    }

    private readonly System.Timers.Timer _reconcileTimer = new(4_000);

    // LoadAsync rebuilds every row instance from scratch and resets SelectedCount/ConflictRows/
    // BatchProgress, so a bare reload on a 4s timer would wipe the user's batch-merge ticks, the
    // highlighted row and a finished batch's outcome badges out from under them — assembling a
    // multi-worktree selection would be impossible. Carry the state that is the user's (or a
    // finished batch's) across the reload, keyed by task id; genuinely new rows come up unticked.
    internal async Task ReconcileTickAsync()
    {
        if (RefreshStatus.IsRunning || CleanupStatus.IsRunning || ForceRemoveStatus.IsRunning || BatchMergeStatus.IsRunning) return;

        // Hold the row INSTANCES, not a value snapshot, and read them back only after the reload:
        // a tick the user lands during the await happens before LoadAsync clears the collection,
        // so the live instance still carries it.
        var previousRows = AllRows.ToList();
        var previousProgress = BatchProgress;

        await LoadAsync();

        var previous = new Dictionary<string, (bool Checked, BatchMergeOutcome Outcome, bool Selected)>(previousRows.Count);
        foreach (var r in previousRows) previous[r.TaskId] = (r.IsChecked, r.MergeOutcome, r.IsSelected);

        WorktreeOverviewRowViewModel? reselect = null;
        foreach (var row in AllRows)
        {
            if (!previous.TryGetValue(row.TaskId, out var state)) continue;
            row.IsChecked = state.Checked;
            row.MergeOutcome = state.Outcome;
            if (state.Outcome == BatchMergeOutcome.Conflict) ConflictRows.Add(row);
            if (state.Selected) reselect = row;
        }

        // SelectedRow still points at a detached pre-reload instance either way — re-point it at
        // the fresh row, or clear it when that worktree is gone rather than leave it dangling.
        if (reselect is not null) SelectRow(reselect);
        else SelectedRow = null;

        BatchProgress = previousProgress;
    }

    public void SelectRow(WorktreeOverviewRowViewModel row)
    {
        if (SelectedRow is not null) SelectedRow.IsSelected = false;
        SelectedRow = row;
        row.IsSelected = true;
    }

    public void Configure(string? listId, string? listName)
    {
        ListIdFilter = listId;
        IsGlobal = listId is null;
        Title = listId is null
            ? Loc.T("vm.worktreesOverview.titleAll")
            : Loc.T("vm.worktreesOverview.titleList", listName ?? Loc.T("vm.worktreesOverview.listFallback"));
    }

    public async Task LoadAsync(CancellationToken ct = default)
    {
        var dtos = await _worker.GetWorktreesOverviewAsync(ListIdFilter);
        var ordered = dtos
            .OrderBy(d => d.State == WorktreeState.Active ? 0 : 1)
            .ThenByDescending(d => d.CreatedAt)
            .Select(Map)
            .ToList();

        Rows.Clear();
        Groups.Clear();
        ConflictRows.Clear();
        SelectedCount = 0;
        BatchProgress = null;
        if (IsGlobal)
        {
            foreach (var grp in ordered.GroupBy(r => (r.ListId, r.ListName))
                                        .OrderBy(g => g.Key.ListName, StringComparer.OrdinalIgnoreCase))
            {
                var group = new WorktreesGroupViewModel { ListId = grp.Key.ListId, ListName = grp.Key.ListName };
                foreach (var row in grp) { HookRow(row); group.Rows.Add(row); }
                Groups.Add(group);
            }
        }
        else
        {
            foreach (var row in ordered) { HookRow(row); Rows.Add(row); }
        }
        await LoadMergeTargetsAsync();
    }

    [RelayCommand]
    private async Task Refresh()
    {
        StatusMessage = null;
        using var op = RefreshStatus.Begin(Loc.T("ops.worktrees.refreshing"));
        await LoadAsync();
    }

    [RelayCommand]
    private async Task CleanupFinished()
    {
        using var op = CleanupStatus.Begin(Loc.T("ops.worktrees.cleaningUp"));
        try
        {
            var result = await _worker.CleanupFinishedWorktreesAsync(ListIdFilter);
            StatusMessage = result is null ? Loc.T("vm.worktreesOverview.cleanupFailed") : Loc.T("vm.worktreesOverview.removed", result.Removed);
            await LoadAsync();
        }
        catch (Exception ex) { StatusMessage = Loc.T("vm.worktreesOverview.cleanupFailedDetailed", ex.Message); }
    }

    [RelayCommand]
    private void Close()
    {
        StopReconcileTick();
        CloseAction?.Invoke();
    }

    /// <summary>Stops the reconcile tick. Also called from the dialog's native-close fallback
    /// (WindowDialogService), which bypasses <see cref="CloseCommand"/> — otherwise an Alt+F4
    /// leaves the timer re-running LoadAsync (git + DB) on an orphaned VM. Idempotent.</summary>
    internal void StopReconcileTick()
    {
        _reconcileTimer.Stop();
        _reconcileTimer.Dispose();
    }

    [RelayCommand]
    private void ShowDiff(WorktreeOverviewRowViewModel? row)
    {
        if (row is null) return;
        var diffVm = _diffVmFactory();
        diffVm.ConfigureWorktree(row.Path, row.BaseCommit);
        ShowDiffAction?.Invoke(diffVm);
    }

    [RelayCommand]
    private void OpenInExplorer(WorktreeOverviewRowViewModel? row)
    {
        var (ok, error) = ShellOpen.Path(row?.Path);
        if (!ok && error is not null)
            ErrorReported?.Invoke(Loc.T("vm.worktreesOverview.openInExplorerFailed", error));
    }

    [RelayCommand]
    private async Task Merge(WorktreeOverviewRowViewModel? row)
    {
        if (row is null || row.State != WorktreeState.Active) return;
        if (ResolveMergeVm is null || ShowMergeAction is null) return;
        var mergeVm = ResolveMergeVm();
        await mergeVm.InitializeAsync(row.TaskId, row.TaskTitle);
        await ShowMergeAction(mergeVm);
        await LoadAsync();
    }

    [RelayCommand]
    private void JumpToTask(WorktreeOverviewRowViewModel? row)
    {
        if (row is null) return;
        JumpToTaskAction?.Invoke(row.ListId, row.TaskId);
        CloseAction?.Invoke();
    }

    [RelayCommand]
    private async Task Discard(WorktreeOverviewRowViewModel? row)
    {
        if (row is null || row.State != WorktreeState.Active) return;
        if (ConfirmAction is not null && !await ConfirmAction(Loc.T("vm.worktreesOverview.discardConfirm", row.TaskTitle))) return;

        var (ok, err) = await _worker.SetWorktreeStateAsync(row.TaskId, WorktreeState.Discarded);
        if (ok) row.State = WorktreeState.Discarded;
        else StatusMessage = err ?? Loc.T("vm.worktreesOverview.discardFailed");
    }

    [RelayCommand]
    private async Task Keep(WorktreeOverviewRowViewModel? row)
    {
        if (row is null || row.State != WorktreeState.Active) return;
        var (ok, err) = await _worker.SetWorktreeStateAsync(row.TaskId, WorktreeState.Kept);
        if (ok) row.State = WorktreeState.Kept;
        else StatusMessage = err ?? Loc.T("vm.worktreesOverview.keepFailed");
    }

    private bool CanForceRemove(WorktreeOverviewRowViewModel? row) => !ForceRemoveStatus.IsRunning;

    [RelayCommand(CanExecute = nameof(CanForceRemove))]
    private async Task ForceRemove(WorktreeOverviewRowViewModel? row)
    {
        if (row is null) return;
        if (row.IsRunning) { StatusMessage = Loc.T("vm.worktreesOverview.cannotForceRunning"); return; }
        if (ConfirmAction is not null && !await ConfirmAction($"Force remove worktree for '{row.TaskTitle}'? This deletes the directory and branch.")) return;

        using var op = ForceRemoveStatus.Begin(Loc.T("ops.worktrees.forceRemoving"));
        ForceRemoveResultDto? result;
        try
        {
            result = await _worker.ForceRemoveWorktreeAsync(row.TaskId);
        }
        catch (Exception ex)
        {
            StatusMessage = Loc.T("vm.worktreesOverview.forceRemoveFailedDetailed", ex.Message);
            return;
        }
        if (result is null || !result.Removed)
        {
            StatusMessage = result?.Reason ?? Loc.T("vm.worktreesOverview.forceRemoveFailed");
            return;
        }
        if (IsGlobal)
        {
            foreach (var grp in Groups)
            {
                var idx = grp.Rows.IndexOf(row);
                if (idx >= 0) { grp.Rows.RemoveAt(idx); break; }
            }
        }
        else
        {
            Rows.Remove(row);
        }
    }

    [RelayCommand]
    private Task CopyBranch(WorktreeOverviewRowViewModel? row) => CopyToClipboardAsync(row?.BranchName);

    [RelayCommand]
    private Task CopyPath(WorktreeOverviewRowViewModel? row) => CopyToClipboardAsync(row?.Path);

    private static async Task CopyToClipboardAsync(string? text)
    {
        if (string.IsNullOrEmpty(text)) return;
        if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop &&
            desktop.MainWindow?.Clipboard is { } clipboard)
        {
            try { await clipboard.SetTextAsync(text); } catch { }
        }
    }

    private static WorktreeOverviewRowViewModel Map(WorktreeOverviewDto d) => new()
    {
        TaskId = d.TaskId, TaskTitle = d.TaskTitle, TaskStatus = d.TaskStatus,
        ListId = d.ListId, ListName = d.ListName,
        Path = d.Path, BranchName = d.BranchName, BaseCommit = d.BaseCommit, State = d.State,
        DiffStat = d.DiffStat, CreatedAt = d.CreatedAt, PathExistsOnDisk = d.PathExistsOnDisk,
    };

    public IEnumerable<WorktreeOverviewRowViewModel> AllRows =>
        IsGlobal ? Groups.SelectMany(g => g.Rows) : Rows;

    private void HookRow(WorktreeOverviewRowViewModel row)
    {
        row.PropertyChanged += (_, e) =>
        {
            if (e.PropertyName is nameof(WorktreeOverviewRowViewModel.IsChecked)
                               or nameof(WorktreeOverviewRowViewModel.State))
                RecomputeSelected();
        };
    }

    private void RecomputeSelected() =>
        SelectedCount = AllRows.Count(r => r.IsChecked && r.IsActive);

    // Test seam: adds a row to the flat list with selection tracking wired up.
    internal void AddRowForTest(WorktreeOverviewRowViewModel row)
    {
        HookRow(row);
        Rows.Add(row);
    }

    private async Task LoadMergeTargetsAsync()
    {
        var anchor = AllRows.FirstOrDefault(r => r.IsActive);
        if (anchor is null) { MergeTargets.Clear(); SelectedTarget = null; return; }
        try
        {
            var targets = await _worker.GetMergeTargetsAsync(anchor.TaskId);
            MergeTargets.Clear();
            if (targets is null) { SelectedTarget = null; return; }
            foreach (var b in targets.LocalBranches) MergeTargets.Add(b);
            SelectedTarget = MergeTargets.Contains(targets.DefaultBranch)
                ? targets.DefaultBranch
                : MergeTargets.FirstOrDefault();
        }
        catch { MergeTargets.Clear(); SelectedTarget = null; }
    }

    private bool CanMergeAll() => !BatchMergeStatus.IsRunning && SelectedCount > 0 && !string.IsNullOrWhiteSpace(SelectedTarget);

    [RelayCommand(CanExecute = nameof(CanMergeAll))]
    private Task MergeAll() => MergeSelectedAsync(_worker.MergeTaskAsync);

    [RelayCommand]
    private void ResolveConflict(WorktreeOverviewRowViewModel? row)
    {
        if (row is null) return;
        _ = _merge.ResolveConflictAsync(row.TaskId, SelectedTarget ?? "");
    }

    [RelayCommand]
    private void ToggleSelectAll()
    {
        var actives = AllRows.Where(r => r.IsActive).ToList();
        var allChecked = actives.Count > 0 && actives.All(r => r.IsChecked);
        foreach (var r in actives) r.IsChecked = !allChecked;
    }

    public async Task MergeSelectedAsync(
        Func<string, string, bool, string, Task<MergeResultDto>> mergeFn,
        CancellationToken ct = default)
    {
        var target = SelectedTarget;
        if (string.IsNullOrWhiteSpace(target)) return;

        var selected = AllRows.Where(r => r.IsChecked && r.IsActive).ToList();
        if (selected.Count == 0) return;

        ConflictRows.Clear();
        var done = 0;
        using var op = BatchMergeStatus.Begin(Loc.T("ops.worktrees.batchMerging", done, selected.Count));
        foreach (var row in selected)
        {
            ct.ThrowIfCancellationRequested();
            row.MergeOutcome = BatchMergeOutcome.Merging;
            BatchMergeStatus.Report(Loc.T("ops.worktrees.batchMerging", ++done, selected.Count));

            MergeResultDto result;
            try
            {
                // Blank message: the worker builds the conventional default per task.
                result = await mergeFn(row.TaskId, target!, false, "");
            }
            catch
            {
                row.MergeOutcome = BatchMergeOutcome.Failed;
                continue;
            }

            switch (result.Status)
            {
                case "merged":
                    row.MergeOutcome = BatchMergeOutcome.Merged;
                    row.State = WorktreeState.Merged;
                    row.IsChecked = false;
                    break;
                case "conflict":
                    row.MergeOutcome = BatchMergeOutcome.Conflict;
                    ConflictRows.Add(row);
                    break;
                case "blocked":
                    row.MergeOutcome = BatchMergeOutcome.Blocked;
                    break;
                case "verify_failed":
                    // The merge landed (so the worktree really is merged), but the list's
                    // verify command failed and the task was kept out of Done. Reporting
                    // this as a plain Failed would claim the merge didn't happen.
                    row.MergeOutcome = BatchMergeOutcome.VerifyFailed;
                    row.State = WorktreeState.Merged;
                    row.IsChecked = false;
                    break;
                default:
                    row.MergeOutcome = BatchMergeOutcome.Failed;
                    break;
            }
        }
        // Batch-Merge OperationStatus.ShowIndicator (and its live "i/n" label) hides once this
        // method returns and `op` disposes — BatchProgress is the post-run summary that survives
        // after the indicator disappears, not a duplicate of the live ticker.
        BatchProgress = Loc.T("vm.worktreesOverview.batchDone",
            selected.Count(r => r.MergeOutcome == BatchMergeOutcome.Merged), ConflictRows.Count);
    }
}
