using System;
using System.Collections.ObjectModel;
using System.IO;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data.Git;
using ClaudeDo.Ui;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;

namespace ClaudeDo.Ui.ViewModels.Modals;

public enum DiffViewerMode { Files, Planning }

/// <summary>
/// One read-only diff viewer replacing DiffModal + WorktreeModal + PlanningDiff.
/// <see cref="DiffViewerMode.Files"/> sources (dirty worktree / branch-vs-base / commit
/// range) load the whole diff via <see cref="GitService"/> and present a folder tree;
/// <see cref="DiffViewerMode.Planning"/> loads per-subtask diffs from the worker with a
/// combined integration-branch toggle. The Merge button (branch source) opens the merge
/// form, which routes to the 3-pane resolver on conflict — the resolver itself is untouched.
/// </summary>
public sealed partial class DiffViewerViewModel : ViewModelBase
{
    private readonly GitService _git;
    private readonly IWorkerClient _worker;
    private readonly AppSettings _settings;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(IsPlanning))]
    [NotifyPropertyChangedFor(nameof(ShowMerge))]
    [NotifyCanExecuteChangedFor(nameof(MergeCommand))]
    private DiffViewerMode _mode = DiffViewerMode.Files;

    public bool IsPlanning => Mode == DiffViewerMode.Planning;

    // ── File-source config ──────────────────────────────────────────────────
    public string? WorktreePath { get; set; }
    public string? BaseRef { get; set; }
    public string? HeadCommit { get; set; }
    public bool FromCommitRange { get; set; }
    public string? TaskId { get; set; }
    public string TaskTitle { get; set; } = "";
    public Func<MergeModalViewModel, Task>? ShowMergeModal { get; set; }
    public Func<MergeModalViewModel>? ResolveMergeVm { get; set; }

    // ── Planning-source config ──────────────────────────────────────────────
    private string? _planningTaskId;
    private string _targetBranch = "";

    // ── Left pane ───────────────────────────────────────────────────────────
    public ObservableCollection<DiffTreeNodeViewModel> FileTree { get; } = new();
    public ObservableCollection<SubtaskDiffRow> Subtasks { get; } = new();
    [ObservableProperty] private DiffTreeNodeViewModel? _selectedNode;
    [ObservableProperty] private SubtaskDiffRow? _selectedSubtask;

    // ── Right pane ──────────────────────────────────────────────────────────
    [ObservableProperty] private DiffFileViewModel? _selectedFile;       // Files mode
    // Planning mode: one entry per file so each gets its own editor and grammar.
    public ObservableCollection<DiffFileViewModel> PlanningFiles { get; } = new();
    [ObservableProperty] private string _displayedDiff = "";
    [ObservableProperty] private string? _statusMessage;

    // ── View toggles (persisted to ui.config.json) ──────────────────────────
    [ObservableProperty] private bool _isSplitView;
    [ObservableProperty] private bool _wrapLines;

    // The layout picker is a segmented switch, so each segment sets its own mode rather than
    // flipping a shared flag — clicking the already-active segment must be a no-op.
    [RelayCommand] private void ShowUnified() => IsSplitView = false;
    [RelayCommand] private void ShowSplit() => IsSplitView = true;

    partial void OnIsSplitViewChanged(bool value)
    {
        _settings.DiffViewMode = value ? "split" : "unified";
        PersistViewPreferences();
    }

    partial void OnWrapLinesChanged(bool value)
    {
        _settings.DiffWrapLines = value;
        PersistViewPreferences();
    }

    /// A failed preference write must never take the diff viewer down with it; the toggle
    /// still works for this session, it just won't survive a restart.
    private void PersistViewPreferences()
    {
        try { _settings.Save(); }
        catch (IOException) { }
        catch (UnauthorizedAccessException) { }
    }

    // ── Planning combined toggle ────────────────────────────────────────────
    [ObservableProperty] private bool _isCombinedMode;
    [ObservableProperty] private string? _combinedWarning;
    [ObservableProperty] private bool _isLoadingCombined;

    public Action? CloseAction { get; set; }

    // Parsing a large diff (UnifiedDiffParser.Parse + DiffTree.Build, both pure CPU work) is
    // offloaded to a threadpool thread so it never blocks the UI dispatcher.
    public OperationStatus ParseOp { get; } = new();

    // Guards the fire-and-forget planning parse below against a stale write: a second
    // DisplayedDiff change while the first parse is still running must not clobber PlanningFiles.
    private int _planningFilesGeneration;

    public DiffViewerViewModel(GitService git, IWorkerClient worker, AppSettings settings)
    {
        _git = git;
        _worker = worker;
        _settings = settings;
        _isSplitView = string.Equals(settings.DiffViewMode, "split", StringComparison.OrdinalIgnoreCase);
        _wrapLines = settings.DiffWrapLines;
    }

    [RelayCommand]
    private void Close() => CloseAction?.Invoke();

    // ── Configuration (called by the doors) ─────────────────────────────────

    public void ConfigureWorktree(string worktreePath, string? baseRef, string? taskId = null, string taskTitle = "")
    {
        Mode = DiffViewerMode.Files;
        WorktreePath = worktreePath;
        BaseRef = string.IsNullOrEmpty(baseRef) ? null : baseRef;
        TaskId = taskId;
        TaskTitle = taskTitle;
    }

    public void ConfigureCommitRange(string repoDir, string? baseRef, string? headCommit,
                                     string? taskId = null, string taskTitle = "")
    {
        Mode = DiffViewerMode.Files;
        WorktreePath = repoDir;
        BaseRef = baseRef;
        HeadCommit = headCommit;
        FromCommitRange = true;
        TaskId = taskId;
        TaskTitle = taskTitle;
    }

    public void ConfigurePlanning(string planningTaskId, string targetBranch)
    {
        Mode = DiffViewerMode.Planning;
        _planningTaskId = planningTaskId;
        _targetBranch = targetBranch;
    }

    // ── Load ────────────────────────────────────────────────────────────────

    public Task LoadAsync(CancellationToken ct = default) =>
        Mode == DiffViewerMode.Planning ? LoadPlanningAsync() : LoadFilesAsync(ct);

    // Shared by both parse call sites (LoadFilesAsync, ApplyParsedPlanningFilesAsync) so the
    // ParseOp begin/offload/end ceremony isn't duplicated. Begin runs synchronously on the UI
    // thread (the caller hasn't awaited anything yet), and the `using` guarantees End runs when
    // the offloaded parse finishes — even if the caller's own result turns out to be stale.
    private async Task<T> ParseOffUiThreadAsync<T>(Func<T> parse, CancellationToken ct = default)
    {
        using (ParseOp.Begin(Loc.T("ops.diff.parsing")))
            return await Task.Run(parse, ct);
    }

    private async Task LoadFilesAsync(CancellationToken ct)
    {
        FileTree.Clear();
        SelectedNode = null;
        SelectedFile = null;
        StatusMessage = null;

        if ((FromCommitRange && (BaseRef is null || HeadCommit is null)) || WorktreePath is null)
        {
            StatusMessage = Loc.T("vm.diff.unavailable");
            return;
        }

        string raw;
        try
        {
            raw = FromCommitRange && BaseRef is not null && HeadCommit is not null
                ? await _git.GetCommitRangeDiffAsync(WorktreePath, BaseRef, HeadCommit, ct: ct)
                : BaseRef is not null
                    ? await _git.GetBranchDiffAsync(WorktreePath, BaseRef, ct: ct)
                    : await _git.GetDiffAsync(WorktreePath, ct: ct);
        }
        catch (Exception ex)
        {
            StatusMessage = Loc.T("vm.diff.loadFailed", ex.Message);
            return;
        }

        if (string.IsNullOrWhiteSpace(raw))
        {
            StatusMessage = Loc.T("vm.diff.noChanges");
            return;
        }

        var (files, tree) = await ParseOffUiThreadAsync(() =>
        {
            var parsed = UnifiedDiffParser.Parse(raw);
            return (parsed, DiffTree.Build(parsed));
        }, ct);

        foreach (var node in tree)
            FileTree.Add(node);

        SelectedNode = DiffTree.FirstLeaf(FileTree);
        if (files.Count == 0) StatusMessage = Loc.T("vm.diff.noChanges");
    }

    partial void OnSelectedNodeChanged(DiffTreeNodeViewModel? value)
    {
        if (value is { IsDirectory: false, File: { } f })
            SelectedFile = f;
    }

    private async Task LoadPlanningAsync()
    {
        if (_planningTaskId is null) return;
        var items = await _worker.GetPlanningAggregateAsync(_planningTaskId);
        Subtasks.Clear();
        foreach (var i in items)
            Subtasks.Add(new SubtaskDiffRow(i.SubtaskId, i.Title, i.DiffStat, i.UnifiedDiff));
        SelectedSubtask = Subtasks.FirstOrDefault();
    }

    partial void OnSelectedSubtaskChanged(SubtaskDiffRow? value)
    {
        if (!IsCombinedMode)
            DisplayedDiff = value?.UnifiedDiff ?? "";
    }

    [RelayCommand]
    private async Task ToggleCombinedAsync()
    {
        if (IsCombinedMode)
        {
            IsLoadingCombined = true;
            try
            {
                var result = await _worker.BuildPlanningIntegrationBranchAsync(_planningTaskId!, _targetBranch);
                if (result is null)
                {
                    DisplayedDiff = "";
                    CombinedWarning = Loc.T("vm.planningDiff.hubError");
                }
                else if (result.Success)
                {
                    DisplayedDiff = result.UnifiedDiff ?? "";
                    CombinedWarning = null;
                }
                else
                {
                    var files = result.ConflictedFiles?.Count ?? 0;
                    CombinedWarning = Loc.T("vm.planningDiff.conflict", result.FirstConflictSubtaskId ?? "", files);
                    DisplayedDiff = "";
                }
            }
            catch (Exception ex)
            {
                DisplayedDiff = "";
                CombinedWarning = Loc.T("vm.planningDiff.buildFailed", ex.Message);
            }
            finally
            {
                IsLoadingCombined = false;
            }
        }
        else
        {
            DisplayedDiff = SelectedSubtask?.UnifiedDiff ?? "";
            CombinedWarning = null;
        }
    }

    partial void OnIsCombinedModeChanged(bool value) => ToggleCombinedCommand.Execute(null);

    // DisplayedDiff is set from four places (OnSelectedSubtaskChanged plus three branches of
    // ToggleCombinedAsync), and OnSelectedSubtaskChanged is itself a synchronous, non-awaitable
    // callback — funneling the offload through this single change handler covers all of them
    // without touching every call site. Fire-and-forget is safe because the generation counter
    // below discards a parse whose result is no longer the current DisplayedDiff.
    partial void OnDisplayedDiffChanged(string value)
    {
        var generation = ++_planningFilesGeneration;
        _ = ApplyParsedPlanningFilesAsync(value, generation);
    }

    private async Task ApplyParsedPlanningFilesAsync(string diff, int generation)
    {
        var files = await ParseOffUiThreadAsync(() => UnifiedDiffParser.Parse(diff));
        if (generation != _planningFilesGeneration) return;

        PlanningFiles.Clear();
        foreach (var file in files)
            PlanningFiles.Add(file);
    }

    // ── Merge (Files mode, branch source) ───────────────────────────────────

    /// Whether the Merge button is offered — only a live branch source with a task and the
    /// merge delegates wired (set before the view binds, so a plain computed read suffices).
    public bool ShowMerge =>
        Mode == DiffViewerMode.Files
        && !string.IsNullOrEmpty(TaskId)
        && ShowMergeModal is not null
        && ResolveMergeVm is not null;

    private bool CanMerge() => ShowMerge;

    [RelayCommand(CanExecute = nameof(CanMerge))]
    private async Task MergeAsync()
    {
        if (TaskId is null || ShowMergeModal is null || ResolveMergeVm is null) return;
        var vm = ResolveMergeVm();
        await vm.InitializeAsync(TaskId, TaskTitle);
        await ShowMergeModal(vm);
        // The diff is stale once the worktree merged away or a conflict opened the editor.
        if (vm.Merged || vm.RoutedToResolver) CloseAction?.Invoke();
    }
}
