using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data;
using ClaudeDo.Data.Filtering;
using ClaudeDo.Data.Models;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals;
using ClaudeDo.Ui.ViewModels.Modals.Settings;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace ClaudeDo.Ui.ViewModels.Islands;

public enum ListKind { Smart, Virtual, User }

/// <summary>Confirmed handler run: the scope list and the ordered selected task ids.</summary>
public sealed record MergeHelperRequest(string ListId, IReadOnlyList<string> TaskIds);

public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
{
    /// <summary>The day-log list. A smart list with no tasks in it: selecting it makes the tasks
    /// island show the notes editor instead of rows.</summary>
    public const string NotesListId = "smart:notes";

    private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
    private readonly IServiceProvider? _services;
    private readonly IWorkerClient? _worker;
    private static readonly TaskListFilterRegistry _filters = new();

    public event EventHandler? SelectionChanged;
    public event EventHandler? FocusSearchRequested;
    public void RequestFocusSearch() => FocusSearchRequested?.Invoke(this, EventArgs.Empty);

    // mirrors TasksIslandViewModel.ErrorReported — surfaces modal-owned failures in the footer strip.
    public event Action<string>? ErrorReported;

    // Same strip, but for an action that succeeded and has a result worth showing.
    public event Action<string>? SuccessReported;

    public IDialogService? Dialogs { get; set; }

    [RelayCommand]
    private async Task OpenSettings()
    {
        if (Dialogs is null || _services is null) return;
        var settingsVm = _services.GetRequiredService<SettingsModalViewModel>();
        await settingsVm.LoadAsync();
        await Dialogs.ShowSettingsAsync(settingsVm);
    }

    [RelayCommand]
    private async System.Threading.Tasks.Task OpenListSettingsAsync(ListNavItemViewModel? row)
    {
        if (row is null || Dialogs is null || _services is null) return;
        var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id;
        var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
        await vm.LoadAsync(rawId, row.Name, row.WorkingDir, row.DefaultCommitType, row.IsManual, row.FindingsTracked, row.IconKey);
        await Dialogs.ShowListSettingsAsync(vm);
        if (vm.Deleted) await LoadAsync();
        else await RefreshRowAsync(row.Id);
    }

    [RelayCommand]
    private async System.Threading.Tasks.Task OpenRepoImportAsync()
    {
        if (Dialogs is null || _services is null) return;
        var vm = _services.GetRequiredService<RepoImportModalViewModel>();
        vm.ErrorReported += msg => ErrorReported?.Invoke(msg);
        await vm.LoadAsync();
        await Dialogs.ShowRepoImportAsync(vm);
        await LoadAsync();
    }

    private bool _worktreesOverviewOpen;

    [RelayCommand]
    private async Task OpenWorktreesOverviewAsync(ListNavItemViewModel? row)
    {
        if (row is null || Dialogs is null || _services is null) return;
        if (row.Kind != ListKind.User) return;
        if (_worktreesOverviewOpen) return;
        _worktreesOverviewOpen = true;
        try
        {
            var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id;
            var vm = _services.GetRequiredService<WorktreesOverviewModalViewModel>();
            vm.ErrorReported += msg => ErrorReported?.Invoke(msg);
            vm.Configure(rawId, row.Name);
            await vm.LoadAsync();
            await Dialogs.ShowWorktreesOverviewAsync(vm);
        }
        finally { _worktreesOverviewOpen = false; }
    }

    /// <summary>Raised after the merge-helper selection dialog is confirmed; the shell opens the ConPTY tile.</summary>
    public event Action<MergeHelperRequest>? LetClaudeHandleRequested;

    [RelayCommand]
    private async Task LetClaudeHandleListAsync(ListNavItemViewModel? row)
    {
        if (row is null || Dialogs is null || _services is null) return;
        if (row.Kind != ListKind.User || string.IsNullOrWhiteSpace(row.WorkingDir)) return;
        var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id;
        var vm = _services.GetRequiredService<MergeHelperSelectionModalViewModel>();
        vm.Configure(rawId, row.Name);
        await vm.LoadAsync();
        var ids = await Dialogs.ShowMergeHelperSelectionAsync(vm);
        if (ids is { Count: > 0 })
            LetClaudeHandleRequested?.Invoke(new MergeHelperRequest(rawId, ids));
    }

    /// <summary>Raised once per list after the review-scoped selection dialog is confirmed;
    /// the shell opens one merge-phase ConPTY tile per request.</summary>
    public event Action<MergeHelperRequest>? MergeReviewsRequested;

    // Review-list variant of LetClaudeHandleListAsync: offers every WaitingForReview task across
    // all repo-linked lists and starts one merge-phase handler session per list picked.
    [RelayCommand]
    private async Task MergeReviewsAsync()
    {
        if (Dialogs is null || _services is null) return;
        var vm = _services.GetRequiredService<MergeHelperSelectionModalViewModel>();
        vm.ConfigureForReview();
        await vm.LoadAsync();
        var ids = await Dialogs.ShowMergeHelperSelectionAsync(vm);
        if (ids is not { Count: > 0 }) return;
        foreach (var (listId, taskIds) in vm.GroupSelectionByList(ids))
            MergeReviewsRequested?.Invoke(new MergeHelperRequest(listId, taskIds));
    }

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

    [RelayCommand]
    private void OpenFindings(ListNavItemViewModel? row)
    {
        var dir = row?.WorkingDir;
        if (string.IsNullOrWhiteSpace(dir)) return;
        var findings = System.IO.Path.Combine(dir, ".claudedo");
        if (!System.IO.Directory.Exists(findings))
        {
            ErrorReported?.Invoke(Loc.T("vm.lists.findingsNotFound"));
            return;
        }
        try
        {
            System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
            {
                FileName = findings,
                UseShellExecute = true,
            });
        }
        catch (Exception ex) { ErrorReported?.Invoke(Loc.T("vm.lists.findingsOpenFailed", ex.Message)); }
    }

    [RelayCommand]
    private void OpenInTerminal(ListNavItemViewModel? row)
    {
        var dir = row?.WorkingDir;
        if (string.IsNullOrWhiteSpace(dir) || !System.IO.Directory.Exists(dir)) return;
        // Trailing separator + ArgumentList, not string interpolation: "C:\repo\" would parse as
        // C:\repo" and wt would refuse it as a starting directory (Paths.TrimTrailingSeparator).
        // Rows loaded before ListRepository normalized on write can still carry one.
        dir = Paths.TrimTrailingSeparator(dir)!;
        ForegroundHelper.AllowAny();
        try
        {
            var psi = new System.Diagnostics.ProcessStartInfo
            {
                FileName = "wt.exe",
                UseShellExecute = true,
            };
            psi.ArgumentList.Add("-d");
            psi.ArgumentList.Add(dir);
            System.Diagnostics.Process.Start(psi);
        }
        catch
        {
            // Windows Terminal not installed — fall back to a plain console at the directory.
            try
            {
                System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
                {
                    FileName = "cmd.exe",
                    WorkingDirectory = dir,
                    UseShellExecute = true,
                });
            }
            catch { /* best-effort */ }
        }
    }

    public ObservableCollection<ListNavItemViewModel> Items { get; } = new();
    public ObservableCollection<ListNavItemViewModel> SmartLists { get; } = new();
    public ObservableCollection<ListNavItemViewModel> UserLists { get; } = new();

    [ObservableProperty] private string _searchText = "";
    [ObservableProperty] private ListNavItemViewModel? _selectedList;

    /// <summary>True whenever no User list has a linked <c>WorkingDir</c> — drives the no-repo
    /// banner. Smart/Virtual lists never count (scope intentionally narrower than
    /// SettingsModalViewModel.HasLinkedRepo, which checks all Lists rows in the DB). Recomputed
    /// after every load and every list CRUD / list-settings save; never persisted, no dismiss
    /// state.</summary>
    [ObservableProperty] private bool _hasNoLinkedRepo = true;

    private void RecomputeHasNoLinkedRepo() =>
        HasNoLinkedRepo = UserLists.All(r => !RepoLinkage.IsLinked(r.WorkingDir));

    public string UserName { get; } = Environment.UserName;
    public string MachineName { get; } = Environment.MachineName;
    public string MachineNameLocal => Loc.T("vm.lists.localSuffix", MachineName);
    public string UserInitials { get; }

    private readonly EventHandler _langChangedHandler;

    public ListsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IServiceProvider? services = null, IWorkerClient? worker = null)
    {
        _dbFactory = dbFactory;
        _services = services;
        _worker = worker;
        var parts = Environment.UserName.Split('.', '_', '-', ' ');
        UserInitials = parts.Length >= 2
            ? $"{parts[0][0]}{parts[1][0]}".ToUpperInvariant()
            : Environment.UserName.Length >= 2
                ? Environment.UserName[..2].ToUpperInvariant()
                : Environment.UserName.ToUpperInvariant();

        if (_worker is not null)
        {
            _worker.ListUpdatedEvent  += id => _ = RefreshRowAsync(id);
            _worker.TaskStartedEvent  += (_slot, _id, _at)             => _ = RefreshCountsAsync();
            _worker.TaskFinishedEvent += (_slot, _id, _status, _at)    => _ = RefreshCountsAsync();
            _worker.TaskUpdatedEvent  += _id                           => _ = RefreshCountsAsync();
            _worker.WorktreeUpdatedEvent += _id                        => _ = RefreshCountsAsync();
            _worker.ConnectionRestoredEvent += ()                      => _ = ReloadAfterReconnectAsync();
        }

        _langChangedHandler = (_, _) => RefreshLocalizedLabels();
        Loc.LanguageChanged += _langChangedHandler;
    }

    public void Dispose()
    {
        Loc.LanguageChanged -= _langChangedHandler;
    }

    private static string? SmartListNameKey(string id) => id switch
    {
        "smart:my-day"    => "vm.lists.smartMyDay",
        "smart:important" => "vm.lists.smartImportant",
        "smart:planned"   => "vm.lists.smartPlanned",
        NotesListId       => "vm.lists.smartNotes",
        "virtual:queued"  => "vm.lists.virtualQueue",
        "virtual:running" => "vm.lists.virtualRunning",
        "virtual:review"  => "vm.lists.virtualReview",
        _                 => null,
    };

    private void RefreshLocalizedLabels()
    {
        foreach (var item in SmartLists)
            if (SmartListNameKey(item.Id) is { } key) item.Name = Loc.T(key);
        OnPropertyChanged(nameof(MachineNameLocal));
    }

    public async Task LoadAsync(CancellationToken ct = default)
    {
        Items.Clear();
        SmartLists.Clear();
        UserLists.Clear();

        var smart = new[]
        {
            new ListNavItemViewModel { Id = "smart:my-day",    Name = Loc.T("vm.lists.smartMyDay"),      Kind = ListKind.Smart,   IconKey = "Sun" },
            new ListNavItemViewModel { Id = "smart:important", Name = Loc.T("vm.lists.smartImportant"),  Kind = ListKind.Smart,   IconKey = "Star" },
            new ListNavItemViewModel { Id = "smart:planned",   Name = Loc.T("vm.lists.smartPlanned"),    Kind = ListKind.Smart,   IconKey = "Calendar" },
            new ListNavItemViewModel { Id = NotesListId,       Name = Loc.T("vm.lists.smartNotes"),      Kind = ListKind.Smart,   IconKey = "Text" },
            new ListNavItemViewModel { Id = "virtual:queued",  Name = Loc.T("vm.lists.virtualQueue"),    Kind = ListKind.Virtual, IconKey = "Inbox" },
            new ListNavItemViewModel { Id = "virtual:running", Name = Loc.T("vm.lists.virtualRunning"),  Kind = ListKind.Virtual, IconKey = "Activity" },
            new ListNavItemViewModel { Id = "virtual:review",  Name = Loc.T("vm.lists.virtualReview"),   Kind = ListKind.Virtual, IconKey = "Eye" },
        };
        foreach (var s in smart) { Items.Add(s); SmartLists.Add(s); }

        var sw = System.Diagnostics.Stopwatch.StartNew();
        var ok = false;
        try
        {
            var ticketsConfigured = await IsTicketsConfiguredAsync();
            await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
            var lists = new ListRepository(ctx);
            var seedNames = new HashSet<string>(new[] { "My Day", "Important", "Planned" });
            foreach (var l in await lists.GetAllAsync(ct))
            {
                if (seedNames.Contains(l.Name)) continue;
                var cfg = await lists.GetConfigAsync(l.Id, ct);
                var item = new ListNavItemViewModel
                {
                    Id = $"user:{l.Id}",
                    Name = l.Name,
                    Kind = ListKind.User,
                    IconKey = l.Icon ?? "Folder",
                    WorkingDir = l.WorkingDir,
                    DefaultCommitType = l.DefaultCommitType,
                    IsManual = l.IsManual,
                    FindingsTracked = l.FindingsTracked,
                    HasTicketProject = ticketsConfigured && cfg?.TicketProjectId > 0,
                };
                Items.Add(item);
                UserLists.Add(item);
            }
            ok = true;
        }
        finally { OperationTiming.Shared.Record("db", "ListsIsland.LoadAsync", sw.Elapsed, ok); }

        RecomputeHasNoLinkedRepo();
        await RefreshCountsAsync(ct);
        SelectedList = Items.FirstOrDefault();
    }

    /// <summary>
    /// A reconnect can have missed offline list creates/renames/deletes (SignalR was down) — unlike
    /// the task/worktree events, which only need a count refresh — so this re-reads the full list
    /// set instead, restoring the prior selection if it still exists.
    /// </summary>
    private async Task ReloadAfterReconnectAsync()
    {
        var selectedId = SelectedList?.Id;
        await LoadAsync();
        if (selectedId is not null && Items.FirstOrDefault(i => i.Id == selectedId) is { } restored)
            SelectedList = restored;
    }

    public async Task RefreshCountsAsync(CancellationToken ct = default)
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        var ok = false;
        try
        {
            await using var ctx = await _dbFactory.CreateDbContextAsync(ct);

            // Single snapshot; counters and the list loader share the same filter strategies.
            var all = await ctx.Tasks.AsNoTracking()
                .Include(t => t.Worktree)
                .ToListAsync(ct);

            foreach (var item in SmartLists)
            {
                var filter = _filters.Resolve(item.Id);
                item.Count = filter is null ? 0 : all.Count(filter.ShouldCount);
            }

            foreach (var item in UserLists)
            {
                var filter = _filters.Resolve(item.Id);
                item.Count = filter is null ? 0 : all.Count(filter.ShouldCount);
            }
            ok = true;
        }
        catch (OperationCanceledException) { throw; }
        catch { /* best-effort refresh */ }
        finally { OperationTiming.Shared.Record("db", "ListsIsland.RefreshCountsAsync", sw.Elapsed, ok); }
    }

    [RelayCommand]
    private void Select(ListNavItemViewModel item) => SelectedList = item;

    [RelayCommand]
    private async Task CreateListAsync()
    {
        var entity = new ListEntity
        {
            Id = Guid.NewGuid().ToString("N"),
            Name = Loc.T("vm.lists.newList"),
            DefaultCommitType = CommitTypeRegistry.DefaultType,
            CreatedAt = DateTime.UtcNow,
        };

        await using (var ctx = await _dbFactory.CreateDbContextAsync())
        {
            var lists = new ListRepository(ctx);
            await lists.AddAsync(entity);
        }

        var item = new ListNavItemViewModel
        {
            Id = $"user:{entity.Id}",
            Name = entity.Name,
            Kind = ListKind.User,
            IconKey = "Folder",
            WorkingDir = entity.WorkingDir,
            DefaultCommitType = entity.DefaultCommitType,
        };
        Items.Add(item);
        UserLists.Add(item);
        SelectedList = item;
        RecomputeHasNoLinkedRepo();

        if (Dialogs is not null && _services is not null)
        {
            var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
            await vm.LoadAsync(entity.Id, entity.Name, entity.WorkingDir, entity.DefaultCommitType, entity.IsManual, entity.FindingsTracked, entity.Icon);
            await Dialogs.ShowListSettingsAsync(vm);
            if (vm.Deleted) await LoadAsync();
            else await RefreshRowAsync(item.Id);
        }
    }

    public void ClearDropHints()
    {
        foreach (var r in UserLists)
        {
            r.DropHintAbove = false;
            r.DropHintBelow = false;
        }
    }

    public void SetDropHint(ListNavItemViewModel target, bool placeBelow)
    {
        foreach (var r in UserLists)
        {
            var isTarget = ReferenceEquals(r, target);
            r.DropHintAbove = isTarget && !placeBelow;
            r.DropHintBelow = isTarget && placeBelow;
        }
    }

    public async Task ReorderAsync(ListNavItemViewModel source, ListNavItemViewModel target, bool placeBelow)
    {
        if (source.Kind != ListKind.User || target.Kind != ListKind.User) return;
        if (ReferenceEquals(source, target)) return;

        MoveWithinCollection(UserLists, source, target, placeBelow);

        var orderedIds = UserLists.Select(i => i.Id["user:".Length..]).ToList();
        var sw = System.Diagnostics.Stopwatch.StartNew();
        var ok = false;
        try
        {
            await using var ctx = await _dbFactory.CreateDbContextAsync();
            var lists = new ListRepository(ctx);
            await lists.ReorderAsync(orderedIds);
            ok = true;
        }
        finally { OperationTiming.Shared.Record("db", "ListsIsland.ReorderAsync", sw.Elapsed, ok); }
    }

    private static void MoveWithinCollection(
        ObservableCollection<ListNavItemViewModel> coll,
        ListNavItemViewModel source,
        ListNavItemViewModel target,
        bool placeBelow)
    {
        var srcIdx = coll.IndexOf(source);
        var tgtIdx = coll.IndexOf(target);
        if (srcIdx < 0 || tgtIdx < 0 || srcIdx == tgtIdx) return;

        var finalIdx = placeBelow ? tgtIdx + 1 : tgtIdx;
        if (srcIdx < finalIdx) finalIdx--;
        if (finalIdx < 0) finalIdx = 0;
        if (finalIdx >= coll.Count) finalIdx = coll.Count - 1;
        if (finalIdx == srcIdx) return;

        coll.Move(srcIdx, finalIdx);
    }

    partial void OnSelectedListChanged(ListNavItemViewModel? value)
    {
        foreach (var i in Items) i.IsActive = ReferenceEquals(i, value);
        SelectionChanged?.Invoke(this, EventArgs.Empty);
    }

    private async System.Threading.Tasks.Task RefreshRowAsync(string rowId)
    {
        try
        {
            // The worker broadcasts the raw list id (ListMcpTools/ConfigMcpTools), never the
            // "user:" prefixed row id used in this VM's own collections — match on the raw id
            // either way so an MCP-driven create/rename is recognized against the loaded row.
            var rawId = rowId.StartsWith("user:") ? rowId["user:".Length..] : rowId;
            var row = UserLists.FirstOrDefault(r =>
                (r.Id.StartsWith("user:") ? r.Id["user:".Length..] : r.Id) == rawId);

            await using var ctx = await _dbFactory.CreateDbContextAsync();
            var lists = new ListRepository(ctx);
            var entity = await lists.GetByIdAsync(rawId);
            if (entity is null) return;
            var cfg = await lists.GetConfigAsync(rawId);
            var hasTicketProject = await IsTicketsConfiguredAsync() && cfg?.TicketProjectId > 0;

            if (row is null)
            {
                await AddRowAsync(lists, entity, hasTicketProject);
                return;
            }

            row.Name = entity.Name;
            row.WorkingDir = entity.WorkingDir;
            row.DefaultCommitType = entity.DefaultCommitType;
            row.IsManual = entity.IsManual;
            row.FindingsTracked = entity.FindingsTracked;
            row.IconKey = entity.Icon ?? "Folder";
            row.HasTicketProject = hasTicketProject;
            RecomputeHasNoLinkedRepo();
        }
        catch { /* best-effort refresh */ }
    }

    private async Task<bool> IsTicketsConfiguredAsync()
    {
        if (!TicketSettingsTabViewModel.FeatureEnabled) return false;
        if (_worker is null) return false;
        try
        {
            var settings = await _worker.GetTicketSettingsAsync();
            return !string.IsNullOrWhiteSpace(settings?.ApiBaseUrl);
        }
        catch { return false; }
    }

    [RelayCommand]
    private async Task ImportTicketsAsync(ListNavItemViewModel? row)
    {
        if (row is null || _worker is null) return;
        var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id;
        try
        {
            var result = await _worker.ImportTicketsAsync(rawId);
            SuccessReported?.Invoke(Loc.T("lists.importTicketsResult", result.Examined, result.Created));
        }
        catch (Exception ex)
        {
            ErrorReported?.Invoke(ex.Message);
        }
    }

    /// <summary>
    /// A `create_list` from a running MCP session has no local row yet — build one from the DB
    /// entity and insert it at its DB sort position, instead of waiting for the next full reload.
    /// </summary>
    private async Task AddRowAsync(ListRepository lists, ListEntity entity, bool hasTicketProject = false)
    {
        var item = new ListNavItemViewModel
        {
            Id = $"user:{entity.Id}",
            Name = entity.Name,
            Kind = ListKind.User,
            IconKey = entity.Icon ?? "Folder",
            WorkingDir = entity.WorkingDir,
            DefaultCommitType = entity.DefaultCommitType,
            IsManual = entity.IsManual,
            FindingsTracked = entity.FindingsTracked,
            HasTicketProject = hasTicketProject,
        };

        var seedNames = new HashSet<string>(new[] { "My Day", "Important", "Planned" });
        var ordered = (await lists.GetAllAsync()).Where(l => !seedNames.Contains(l.Name)).ToList();
        var dbIndex = ordered.FindIndex(l => l.Id == entity.Id);
        var insertAt = dbIndex < 0 ? UserLists.Count : Math.Min(dbIndex, UserLists.Count);

        UserLists.Insert(insertAt, item);
        Items.Insert(SmartLists.Count + insertAt, item);
        RecomputeHasNoLinkedRepo();
    }
}
