using System.Collections.ObjectModel;
using System.ComponentModel;
using ClaudeDo.Data;
using ClaudeDo.Ui.Localization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Ui.ViewModels.Modals;

public sealed partial class MergeHelperTaskRowViewModel : ViewModelBase
{
    public required string Id { get; init; }
    public int Number { get; init; }
    public required string Title { get; init; }
    public required string StatusText { get; init; }

    public bool ShowNumber => Number > 0;
    public string? ParentTaskId { get; init; }

    /// <summary>The owning list — only set in review mode, where rows span lists.</summary>
    public string ListId { get; init; } = "";

    /// <summary>A non-selectable list-name section header (review mode groups rows by list).</summary>
    public bool IsListHeader { get; init; }

    /// <summary>True when the parent is also a candidate and rendered directly above this row.</summary>
    public bool IsChild { get; init; }

    /// <summary>
    /// True when <see cref="ParentTaskId"/> is set but the parent isn't a candidate itself
    /// (e.g. already Done/Cancelled) — the row still renders top-level, flagged with a hint.
    /// </summary>
    public bool IsOrphanChild { get; init; }

    [ObservableProperty] private bool _isSelected;
}

/// <summary>
/// "Let Claude handle it" task picker: lists the non-terminal tasks of a list (or all lists),
/// pre-ticks the actionable ones, and resolves <see cref="Result"/> with the ordered selected
/// task ids (null on cancel).
/// </summary>
public sealed partial class MergeHelperSelectionModalViewModel : ViewModelBase
{
    private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
    private string _listId = "";
    private bool _reviewMode;

    // Parent id -> its child rows, built after load; drives the tick-parent-ticks-children cascade.
    private Dictionary<string, List<MergeHelperTaskRowViewModel>> _childrenByParentId = new();

    // Set while ReconcileTickAsync restores remembered ticks, so replaying a parent's state
    // doesn't cascade over its children's own remembered state.
    private bool _suppressCascade;

    // Only one tick may be in flight. A second one entering while the first awaits LoadAsync
    // would snapshot an already-cleared Tasks collection, restore that emptiness, and let
    // IsTickedByDefault re-tick every row — silently undoing the user's unticks, i.e. exactly
    // what the remember/restore below exists to prevent.
    private bool _tickInFlight;

    // Test seam: awaited inside ReconcileTickAsync after the reload and before the restore, so a
    // test can hold one tick open in exactly the window a second one would corrupt. Null in prod.
    internal Func<Task>? ReconcileTickTestBarrier { get; set; }

    public ObservableCollection<MergeHelperTaskRowViewModel> Tasks { get; } = new();

    [ObservableProperty] private string _scopeLabel = "";

    public bool HasTasks => Tasks.Count > 0;
    public bool CanConfirm => Tasks.Any(t => t.IsSelected);

    public TaskCompletionSource<IReadOnlyList<string>?> Result { get; } = new();
    public Action? CloseAction { get; set; }

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

    public MergeHelperSelectionModalViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory)
    {
        _dbFactory = dbFactory;
        // Phase 3 reconcile tick: this overlay is long-lived (stays open while the user ticks
        // through candidates), so refresh it on the same cadence as TasksIslandViewModel's tick
        // instead of leaving it frozen at the moment it was opened.
        _reconcileTimer.Elapsed += (_, _) =>
            Avalonia.Threading.Dispatcher.UIThread.Post(() => _ = ReconcileTickAsync());
        _reconcileTimer.Start();
    }

    public void Configure(string listId, string listName)
    {
        _listId = listId;
        ScopeLabel = Loc.T("modals.mergeHelper.scopeList", listName);
    }

    /// <summary>Review mode: offers every WaitingForReview task across all repo-linked lists,
    /// grouped under list-name header rows, all pre-ticked — the "merge the review pile" picker.</summary>
    public void ConfigureForReview()
    {
        _reviewMode = true;
        ScopeLabel = Loc.T("modals.mergeHelper.scopeReview");
    }

    /// <summary>Groups a confirmed review-mode selection by owning list, preserving row order —
    /// the caller starts one merge-phase handler session per group.</summary>
    public IReadOnlyList<(string ListId, IReadOnlyList<string> TaskIds)> GroupSelectionByList(IReadOnlyList<string> ids)
    {
        var listByTaskId = Tasks.Where(t => !t.IsListHeader).ToDictionary(t => t.Id, t => t.ListId);
        return ids.Where(listByTaskId.ContainsKey)
            .GroupBy(id => listByTaskId[id])
            .Select(g => (g.Key, (IReadOnlyList<string>)g.ToList()))
            .ToList();
    }

    // LoadAsync rebuilds every row from scratch and re-applies IsTickedByDefault, which would wipe
    // the user's ticks out from under them on every tick — in BOTH directions. Remembering only
    // the ticked ids would silently re-tick a row the user deliberately unticked (Idle/Queued/
    // WaitingForReview/Failed all default to ticked), so carry the full state of every row that
    // already existed across the reload; only genuinely new rows get the default.
    internal async Task ReconcileTickAsync()
    {
        if ((string.IsNullOrEmpty(_listId) && !_reviewMode) || _tickInFlight) return;
        _tickInFlight = true;
        try
        {
            // Hold the row INSTANCES, not a value snapshot, and read their state 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. Snapshotting up front would
            // drop that click.
            var previousRows = Tasks.ToList();
            await LoadAsync();

            if (ReconcileTickTestBarrier is { } barrier) await barrier();

            var previous = new Dictionary<string, bool>(previousRows.Count);
            foreach (var t in previousRows) previous[t.Id] = t.IsSelected;

            // Restore without the parent→children cascade: it would overwrite a child's own
            // remembered state with its parent's, undoing exactly the per-child choice being
            // restored.
            _suppressCascade = true;
            try
            {
                foreach (var t in Tasks)
                    if (previous.TryGetValue(t.Id, out var wasSelected))
                        t.IsSelected = wasSelected;
            }
            finally
            {
                _suppressCascade = false;
            }

            OnPropertyChanged(nameof(CanConfirm));
        }
        finally
        {
            _tickInFlight = false;
        }
    }

    public async Task LoadAsync(CancellationToken ct = default)
    {
        foreach (var row in Tasks) row.PropertyChanged -= OnRowChanged;
        Tasks.Clear();
        _childrenByParentId = new();

        await using var ctx = await _dbFactory.CreateDbContextAsync(ct);

        if (_reviewMode)
        {
            // Every WaitingForReview task of every repo-linked list, grouped under a header row per
            // list. IsManual excludes reminders AND prior handler host tasks. Flat on purpose: a
            // review parent's children are already terminal, so the hierarchy walk below has
            // nothing to nest here.
            var reviewCandidates = await ctx.Tasks.AsNoTracking()
                .Where(t => t.Status == TaskStatus.WaitingForReview && !t.IsManual)
                .Join(ctx.Lists.Where(l => l.WorkingDir != null && l.WorkingDir != ""),
                    t => t.ListId, l => l.Id,
                    (t, l) => new { t.Id, t.Number, t.Title, t.Status, t.ParentTaskId, t.ListId, ListName = l.Name, t.SortOrder, t.CreatedAt })
                .OrderBy(x => x.ListName).ThenBy(x => x.SortOrder).ThenBy(x => x.CreatedAt)
                .ToListAsync(ct);

            foreach (var group in reviewCandidates.GroupBy(x => x.ListId))
            {
                Tasks.Add(new MergeHelperTaskRowViewModel
                {
                    Id = $"header:{group.Key}",
                    Title = group.First().ListName,
                    StatusText = "",
                    ListId = group.Key,
                    IsListHeader = true,
                });
                foreach (var c in group)
                {
                    var reviewRow = new MergeHelperTaskRowViewModel
                    {
                        Id = c.Id,
                        Number = c.Number,
                        Title = c.Title,
                        StatusText = c.Status.ToString(),
                        ParentTaskId = c.ParentTaskId,
                        ListId = c.ListId,
                        IsSelected = true,
                    };
                    reviewRow.PropertyChanged += OnRowChanged;
                    Tasks.Add(reviewRow);
                }
            }

            OnPropertyChanged(nameof(HasTasks));
            OnPropertyChanged(nameof(CanConfirm));
            return;
        }
        var candidates = await ctx.Tasks.AsNoTracking()
            .Where(t => t.Status != TaskStatus.Done && t.Status != TaskStatus.Cancelled)
            .Where(t => t.ListId == _listId)
            // Manual tasks are reminders the user owns — never offer them to the handler.
            .Where(t => !t.IsManual)
            .OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
            .Select(t => new { t.Id, t.Number, t.Title, t.Status, t.ParentTaskId })
            .ToListAsync(ct);

        var candidateIds = candidates.Select(c => c.Id).ToHashSet();
        bool ParentIsCandidate(string? parentId) =>
            !string.IsNullOrEmpty(parentId) && candidateIds.Contains(parentId);

        var childrenByParent = candidates
            .Where(c => ParentIsCandidate(c.ParentTaskId))
            .GroupBy(c => c.ParentTaskId!)
            .ToDictionary(g => g.Key, g => g.ToList());

        // Hierarchy-ordered walk, same idea as TasksIslandViewModel.Regroup: top-level rows
        // (including orphaned children whose parent isn't a candidate) interleaved with their
        // visible children, preserving the SortOrder/CreatedAt order within each level.
        var emitted = new HashSet<string>();
        var ordered = new List<(string Id, int Number, string Title, TaskStatus Status, string? ParentTaskId)>();
        void Emit(string id, int number, string title, TaskStatus status, string? parentTaskId)
        {
            if (!emitted.Add(id)) return;
            ordered.Add((id, number, title, status, parentTaskId));
            if (childrenByParent.TryGetValue(id, out var kids))
                foreach (var kid in kids)
                    Emit(kid.Id, kid.Number, kid.Title, kid.Status, kid.ParentTaskId);
        }
        foreach (var c in candidates.Where(c => !ParentIsCandidate(c.ParentTaskId)))
            Emit(c.Id, c.Number, c.Title, c.Status, c.ParentTaskId);

        foreach (var c in ordered)
        {
            var isChild = ParentIsCandidate(c.ParentTaskId);
            var row = new MergeHelperTaskRowViewModel
            {
                Id = c.Id,
                Number = c.Number,
                Title = c.Title,
                StatusText = c.Status.ToString(),
                ParentTaskId = c.ParentTaskId,
                IsChild = isChild,
                IsOrphanChild = !isChild && !string.IsNullOrEmpty(c.ParentTaskId),
                IsSelected = IsTickedByDefault(c.Status),
            };
            row.PropertyChanged += OnRowChanged;
            Tasks.Add(row);
        }

        _childrenByParentId = Tasks
            .Where(r => r.IsChild)
            .GroupBy(r => r.ParentTaskId!)
            .ToDictionary(g => g.Key, g => g.ToList());

        OnPropertyChanged(nameof(HasTasks));
        OnPropertyChanged(nameof(CanConfirm));
    }

    // Actionable statuses are pre-ticked; Running/WaitingForChildren are listed but unticked
    // (the helper would only poll them). Done/Cancelled never make it into the list.
    internal static bool IsTickedByDefault(TaskStatus status) => status
        is TaskStatus.Idle or TaskStatus.Queued or TaskStatus.WaitingForReview or TaskStatus.Failed;

    private void OnRowChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName != nameof(MergeHelperTaskRowViewModel.IsSelected)) return;

        // Ticking/unticking a parent cascades to its children; a child can still be
        // deselected on its own afterwards without affecting the parent's checkbox —
        // ClaudeDo has no indeterminate/tri-state parent checkbox.
        if (!_suppressCascade
            && sender is MergeHelperTaskRowViewModel row
            && _childrenByParentId.TryGetValue(row.Id, out var children))
        {
            foreach (var child in children) child.IsSelected = row.IsSelected;
        }

        OnPropertyChanged(nameof(CanConfirm));
    }

    [RelayCommand]
    private void SelectAll()
    {
        foreach (var t in Tasks)
            if (!t.IsListHeader) t.IsSelected = true;
    }

    [RelayCommand]
    private void SelectNone()
    {
        foreach (var t in Tasks) t.IsSelected = false;
    }

    [RelayCommand]
    private void Confirm()
    {
        StopReconcileTick();
        Result.TrySetResult(Tasks.Where(t => t.IsSelected && !t.IsListHeader).Select(t => t.Id).ToList());
        CloseAction?.Invoke();
    }

    [RelayCommand]
    private void Cancel()
    {
        StopReconcileTick();
        Result.TrySetResult(null);
        CloseAction?.Invoke();
    }

    // Also called from the dialog's native-close fallback (WindowDialogService), which resolves
    // Result without going through Confirm/Cancel. Idempotent — Timer.Stop()/Dispose() tolerate
    // repeated calls.
    internal void StopReconcileTick()
    {
        _reconcileTimer.Stop();
        _reconcileTimer.Dispose();
    }
}
