using System.Collections.ObjectModel;
using System.Text;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Helpers;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace ClaudeDo.Ui.ViewModels.Islands;

public sealed partial class DetailsIslandViewModel : ViewModelBase
{
    private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
    private readonly IWorkerClient _worker;
    private readonly IServiceProvider _services;

    // Current task row (set by IslandsShellViewModel via Bind)
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(EnqueueCommand))]
    [NotifyCanExecuteChangedFor(nameof(DequeueCommand))]
    [NotifyCanExecuteChangedFor(nameof(ResetAndRetryCommand))]
    [NotifyCanExecuteChangedFor(nameof(ReviewCombinedDiffCommand))]
    [NotifyPropertyChangedFor(nameof(TaskIdBadge))]
    private TaskRowViewModel? _task;

    // Editable fields
    [ObservableProperty] private string _editableTitle = "";
    [ObservableProperty] private string _editableDescription = "";
    [ObservableProperty] private bool _isEditingDescription;
    [ObservableProperty] private bool _isDescriptionExpanded = true;

    public bool IsDescriptionEditorVisible => IsDescriptionExpanded && IsEditingDescription;
    public bool IsDescriptionPreviewVisible => IsDescriptionExpanded && !IsEditingDescription;

    partial void OnIsDescriptionExpandedChanged(bool value)
    {
        OnPropertyChanged(nameof(IsDescriptionEditorVisible));
        OnPropertyChanged(nameof(IsDescriptionPreviewVisible));
    }

    partial void OnIsEditingDescriptionChanged(bool value)
    {
        OnPropertyChanged(nameof(IsDescriptionEditorVisible));
        OnPropertyChanged(nameof(IsDescriptionPreviewVisible));
    }

    [RelayCommand]
    private void ToggleEditDescription() => IsEditingDescription = !IsEditingDescription;

    [RelayCommand]
    private void ToggleDescriptionExpanded() => IsDescriptionExpanded = !IsDescriptionExpanded;

    // Short task-id badge, e.g. "#T1A"
    public string TaskIdBadge => Task != null ? $"#T{Task.Id[..Math.Min(3, Task.Id.Length)].ToUpperInvariant()}" : "";

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(EnqueueCommand))]
    [NotifyCanExecuteChangedFor(nameof(DequeueCommand))]
    [NotifyCanExecuteChangedFor(nameof(ResetAndRetryCommand))]
    [NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
    private string _agentStatusLabel = "Idle";
    public bool IsIdle      => AgentStatusLabel == "Idle";
    public bool IsQueued    => AgentStatusLabel == "Queued";
    public bool IsRunning   => AgentStatusLabel == "Running";
    public bool IsDone      => AgentStatusLabel == "Done";
    public bool IsFailed    => AgentStatusLabel == "Failed";
    public bool IsCancelled => AgentStatusLabel == "Cancelled";

    // Recovery actions: Continue (resume session) for Failed/Cancelled.
    public bool ShowContinue => IsFailed || IsCancelled;
    // Reset & retry available from any terminal state.
    public bool ShowResetAndRetry => IsFailed || IsCancelled || IsDone;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
    private string? _latestRunSessionId;

    partial void OnAgentStatusLabelChanged(string value)
    {
        OnPropertyChanged(nameof(IsIdle));
        OnPropertyChanged(nameof(IsQueued));
        OnPropertyChanged(nameof(IsRunning));
        OnPropertyChanged(nameof(IsDone));
        OnPropertyChanged(nameof(IsFailed));
        OnPropertyChanged(nameof(IsCancelled));
        OnPropertyChanged(nameof(ShowContinue));
        OnPropertyChanged(nameof(ShowResetAndRetry));
        OnPropertyChanged(nameof(IsAgentSectionEnabled));
    }
    [ObservableProperty] private string? _model;

    // Agent settings overrides
    [ObservableProperty] private string _taskModelSelection = ModelRegistry.TaskInheritSentinel;
    [ObservableProperty] private string _taskSystemPrompt = "";
    [ObservableProperty] private AgentInfo? _taskSelectedAgent;

    [ObservableProperty] private string _effectiveModelHint = "";
    [ObservableProperty] private string _effectiveSystemPromptHint = "";
    [ObservableProperty] private string _effectiveAgentHint = "";

    public System.Collections.ObjectModel.ObservableCollection<string> TaskModelOptions { get; } = new(
        new[] { ModelRegistry.TaskInheritSentinel }.Concat(ModelRegistry.Aliases));

    public System.Collections.ObjectModel.ObservableCollection<AgentInfo> TaskAgentOptions { get; } = new();

    private bool _suppressAgentSave;
    private CancellationTokenSource? _agentSaveCts;

    private bool _suppressDescSave;
    private CancellationTokenSource? _descSaveCts;

    public bool IsAgentSectionEnabled => !IsRunning;

    [ObservableProperty] private string? _worktreePath;
    [ObservableProperty] private string? _worktreeBaseCommit;
    [ObservableProperty] private string? _worktreeStateLabel;
    [ObservableProperty] private string? _branchLine;
    [ObservableProperty] private int _turns;
    [ObservableProperty] private int _tokens;
    [ObservableProperty] private int _diffAdditions;
    [ObservableProperty] private int _diffDeletions;
    [ObservableProperty] private int _commitsOnBranch;

    public string TokensFormatted => Tokens >= 1000 ? $"{Tokens / 1000.0:F1}k" : Tokens.ToString();
    public string ElapsedFormatted => "";  // placeholder — no start-time stored yet

    partial void OnTokensChanged(int value)   => OnPropertyChanged(nameof(TokensFormatted));
    partial void OnDiffAdditionsChanged(int value) => OnPropertyChanged(nameof(DiffMeterRatio));
    partial void OnDiffDeletionsChanged(int value) => OnPropertyChanged(nameof(DiffMeterRatio));

    // 0.0–1.0 additions share for the diff meter
    public double DiffMeterRatio
    {
        get
        {
            var total = DiffAdditions + DiffDeletions;
            return total == 0 ? 0.0 : (double)DiffAdditions / total;
        }
    }

    public ObservableCollection<LogLineViewModel> Log { get; } = new();
    public ObservableCollection<SubtaskRowViewModel> Subtasks { get; } = new();

    [ObservableProperty] private string _newSubtaskTitle = "";

    // Planning merge controls
    [ObservableProperty] private ObservableCollection<string> _mergeTargetBranches = new();
    [ObservableProperty] private string? _selectedMergeTarget;
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(MergeAllCommand))]
    private bool _canMergeAll;
    [ObservableProperty] private string? _mergeAllDisabledReason;
    [ObservableProperty] private string? _mergeAllError;

    // Claude CLI stream-json parser + buffer for partial text deltas
    private readonly StreamLineFormatter _formatter = new();
    private readonly StringBuilder _claudeBuf = new();

    // The task ID we are currently subscribed to for live log messages
    private string? _subscribedTaskId;

    private CancellationTokenSource? _loadCts;

    // Set by shell so CloseDetailCommand can clear SelectedTask
    public Action? CloseDetail { get; set; }

    // Set by shell so DeleteTaskCommand can remove from list
    public Func<TaskRowViewModel, System.Threading.Tasks.Task>? DeleteFromList { get; set; }

    // Set by the view so OpenDiffCommand can show the modal as a dialog
    public Func<DiffModalViewModel, System.Threading.Tasks.Task>? ShowDiffModal { get; set; }

    // Set by the view so OpenDiff can pass through merge requests from the diff modal
    public Func<MergeModalViewModel, System.Threading.Tasks.Task>? ShowMergeModal { get; set; }

    // Set by the view so DeleteTaskCommand can prompt yes/no before deleting
    public Func<string, System.Threading.Tasks.Task<bool>>? ConfirmAsync { get; set; }

    // Set by the view so ReviewCombinedDiffCommand can show the planning diff modal
    public Func<ViewModels.Planning.PlanningDiffViewModel, System.Threading.Tasks.Task>? ShowPlanningDiffModal { get; set; }

    // Set by the view so DeleteTaskCommand can show an error message
    public Func<string, System.Threading.Tasks.Task>? ShowErrorAsync { get; set; }

    private async System.Threading.Tasks.Task RefreshStatusAsync(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 || Task?.Id != taskId) return;

            AgentStatusLabel = entity.Status.ToString();
        }
        catch { }
    }

    public DetailsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker, IServiceProvider services)
    {
        _dbFactory = dbFactory;
        _worker = worker;
        _services = services;

        // Subscribe once; filter by current task id inside the handler
        _worker.TaskMessageEvent += OnTaskMessage;

        // Re-evaluate CanExecute when worker connection flips.
        _worker.PropertyChanged += (_, e) =>
        {
            if (e.PropertyName == nameof(WorkerClient.IsConnected))
            {
                EnqueueCommand.NotifyCanExecuteChanged();
                DequeueCommand.NotifyCanExecuteChanged();
                ResetAndRetryCommand.NotifyCanExecuteChanged();
                ContinueCommand.NotifyCanExecuteChanged();
            }
        };

        // If the task row's live status changes (e.g. TaskStarted/Finished), mirror it.
        _worker.TaskStartedEvent += (slot, taskId, startedAt) =>
        {
            if (Task?.Id == taskId) AgentStatusLabel = "Running";
        };
        _worker.TaskFinishedEvent += (slot, taskId, status, finishedAt) =>
        {
            if (Task?.Id != taskId) return;
            FlushClaudeBuffer();
            Log.Add(new LogLineViewModel
            {
                Kind = LogKind.Done,
                Text = $"── {status.ToUpperInvariant()} · {finishedAt.ToLocalTime():HH:mm:ss} ──",
            });
            AgentStatusLabel = status;
            // Re-query to pick up worktree created during the run.
            _ = RefreshWorktreeAsync(taskId);
        };

        _worker.WorktreeUpdatedEvent += taskId =>
        {
            if (Task?.Id == taskId) _ = RefreshWorktreeAsync(taskId);
            if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
        };

        _worker.TaskUpdatedEvent += taskId =>
        {
            if (Task?.Id == taskId) _ = RefreshStatusAsync(taskId);
            if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
        };

        Subtasks.CollectionChanged += (_, _) =>
        {
            RecomputeCanMergeAll();
            ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
        };
    }

    private void OnTaskMessage(string taskId, string line)
    {
        if (taskId != _subscribedTaskId) return;

        // `[stdout] ...json...` lines are Claude CLI stream-json; parse through the
        // formatter so the user sees human text, not raw JSON.
        if (line.StartsWith("[stdout]", StringComparison.OrdinalIgnoreCase))
        {
            var body = line["[stdout]".Length..].TrimStart();
            var formatted = _formatter.FormatLine(body);
            if (formatted is null) return; // filter noise (message_start, etc.)
            AppendClaudeText(formatted);
            return;
        }

        // Non-stdout tagged lines: flush any buffered text then classify by prefix.
        FlushClaudeBuffer();

        var kind = line.StartsWith("[sys]", StringComparison.OrdinalIgnoreCase)    ? LogKind.Sys
                 : line.StartsWith("[tool]", StringComparison.OrdinalIgnoreCase)   ? LogKind.Tool
                 : line.StartsWith("[claude]", StringComparison.OrdinalIgnoreCase) ? LogKind.Claude
                 : line.StartsWith("[stderr]", StringComparison.OrdinalIgnoreCase) ? LogKind.Stderr
                 : line.StartsWith("[done]", StringComparison.OrdinalIgnoreCase)   ? LogKind.Done
                 : LogKind.Msg;
        Log.Add(new LogLineViewModel { Kind = kind, Text = line });
    }

    private void AppendClaudeText(string chunk)
    {
        _claudeBuf.Append(chunk);
        // Emit a log entry for every completed line; keep the trailing remainder buffered.
        while (true)
        {
            var text = _claudeBuf.ToString();
            var nl = text.IndexOf('\n');
            if (nl < 0) break;
            var piece = text[..nl].TrimEnd('\r');
            if (!string.IsNullOrWhiteSpace(piece))
                Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
            _claudeBuf.Clear();
            _claudeBuf.Append(text[(nl + 1)..]);
        }
    }

    private void FlushClaudeBuffer()
    {
        if (_claudeBuf.Length == 0) return;
        var piece = _claudeBuf.ToString().TrimEnd();
        _claudeBuf.Clear();
        if (!string.IsNullOrWhiteSpace(piece))
            Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
    }

    partial void OnTaskModelSelectionChanged(string value) => QueueAgentSave();
    partial void OnTaskSystemPromptChanged(string value) => QueueAgentSave();
    partial void OnTaskSelectedAgentChanged(AgentInfo? value) => QueueAgentSave();

    partial void OnEditableDescriptionChanged(string value)
    {
        if (_suppressDescSave || Task is null) return;
        _descSaveCts?.Cancel();
        _descSaveCts = new CancellationTokenSource();
        _ = SaveDescriptionAsync(_descSaveCts.Token);
    }

    private async System.Threading.Tasks.Task SaveDescriptionAsync(CancellationToken ct)
    {
        try
        {
            await System.Threading.Tasks.Task.Delay(400, ct);
            if (Task is null) return;
            await using var ctx = _dbFactory.CreateDbContext();
            var repo = new TaskRepository(ctx);
            var entity = await repo.GetByIdAsync(Task.Id);
            if (entity is null) return;
            entity.Description = string.IsNullOrWhiteSpace(EditableDescription) ? null : EditableDescription;
            await repo.UpdateAsync(entity);
        }
        catch (OperationCanceledException) { }
        catch { }
    }

    private void QueueAgentSave()
    {
        if (_suppressAgentSave || Task is null) return;
        _agentSaveCts?.Cancel();
        _agentSaveCts = new CancellationTokenSource();
        var ct = _agentSaveCts.Token;
        _ = SaveAgentSettingsAsync(ct);
    }

    private async System.Threading.Tasks.Task SaveAgentSettingsAsync(CancellationToken ct)
    {
        try
        {
            await System.Threading.Tasks.Task.Delay(300, ct);
            if (Task is null) return;

            var model = TaskModelSelection == ModelRegistry.TaskInheritSentinel ? null : TaskModelSelection;
            var sp = string.IsNullOrWhiteSpace(TaskSystemPrompt) ? null : TaskSystemPrompt;
            var ap = TaskSelectedAgent is null || string.IsNullOrWhiteSpace(TaskSelectedAgent.Path)
                ? null : TaskSelectedAgent.Path;

            await _worker.UpdateTaskAgentSettingsAsync(
                new ClaudeDo.Ui.Services.UpdateTaskAgentSettingsDto(Task.Id, model, sp, ap));
        }
        catch (OperationCanceledException) { }
        catch { }
    }

    private async System.Threading.Tasks.Task LoadAgentSettingsAsync(
        ClaudeDo.Data.Models.TaskEntity entity, CancellationToken ct)
    {
        _suppressAgentSave = true;
        try
        {
            TaskAgentOptions.Clear();
            TaskAgentOptions.Add(new AgentInfo(ModelRegistry.TaskInheritSentinel, "", ""));
            var agents = await _worker.GetAgentsAsync();
            foreach (var a in agents) TaskAgentOptions.Add(a);

            TaskModelSelection = string.IsNullOrWhiteSpace(entity.Model) ? ModelRegistry.TaskInheritSentinel : entity.Model!;
            TaskSystemPrompt = entity.SystemPrompt ?? "";
            TaskSelectedAgent = string.IsNullOrWhiteSpace(entity.AgentPath)
                ? TaskAgentOptions[0]
                : (TaskAgentOptions.FirstOrDefault(a => a.Path == entity.AgentPath) ?? TaskAgentOptions[0]);

            var listCfg = await _worker.GetListConfigAsync(entity.ListId);
            EffectiveModelHint = string.IsNullOrWhiteSpace(listCfg?.Model) ? "(global default)" : listCfg!.Model!;
            EffectiveSystemPromptHint = string.IsNullOrWhiteSpace(listCfg?.SystemPrompt) ? "(none)" : listCfg!.SystemPrompt!;
            EffectiveAgentHint = string.IsNullOrWhiteSpace(listCfg?.AgentPath)
                ? "(none)" : System.IO.Path.GetFileName(listCfg!.AgentPath!);
        }
        finally
        {
            _suppressAgentSave = false;
        }
    }

    public void Bind(TaskRowViewModel? row)
    {
        _loadCts?.Cancel();
        _loadCts?.Dispose();
        _loadCts = new CancellationTokenSource();
        var ct = _loadCts.Token;

        Task = row;
        OnPropertyChanged(nameof(TaskIdBadge));
        Log.Clear();
        Subtasks.Clear();
        MergeTargetBranches.Clear();
        SelectedMergeTarget = null;
        CanMergeAll = false;
        MergeAllDisabledReason = null;
        MergeAllError = null;
        _claudeBuf.Clear();

        if (row == null)
        {
            _subscribedTaskId = null;
            EditableTitle = "";
            EditableDescription = "";
            Model = null;
            WorktreePath = null;
            WorktreeStateLabel = null;
            BranchLine = null;
            AgentStatusLabel = "Idle";
            LatestRunSessionId = null;
            _suppressAgentSave = true;
            try
            {
                TaskModelSelection = ModelRegistry.TaskInheritSentinel;
                TaskSystemPrompt = "";
                TaskSelectedAgent = null;
            }
            finally
            {
                _suppressAgentSave = false;
            }
            EffectiveModelHint = "";
            EffectiveSystemPromptHint = "";
            EffectiveAgentHint = "";
            return;
        }

        _ = BindAsync(row, ct);
    }

    private async System.Threading.Tasks.Task BindAsync(TaskRowViewModel row, CancellationToken ct)
    {
        try
        {
            await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
            var subtaskRepo = new SubtaskRepository(ctx);

            // Own query with Include so WorktreePath/BranchLine are populated.
            var entity = await ctx.Tasks
                .AsNoTracking()
                .Include(t => t.Worktree)
                .FirstOrDefaultAsync(t => t.Id == row.Id, ct);
            ct.ThrowIfCancellationRequested();
            if (entity == null) return;

            EditableTitle = entity.Title;
            _suppressDescSave = true;
            try { EditableDescription = entity.Description ?? ""; }
            finally { _suppressDescSave = false; }
            Model = entity.Model;
            WorktreePath = entity.Worktree?.Path;
            WorktreeBaseCommit = entity.Worktree?.BaseCommit;
            WorktreeStateLabel = entity.Worktree?.State.ToString();
            BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null;
            AgentStatusLabel = entity.Status.ToString();
            await LoadAgentSettingsAsync(entity, ct);
            ct.ThrowIfCancellationRequested();

            var runRepo = new TaskRunRepository(ctx);
            var latestRun = await runRepo.GetLatestByTaskIdAsync(row.Id, ct);
            ct.ThrowIfCancellationRequested();
            LatestRunSessionId = latestRun?.SessionId;

            // Subscribe only after DB load confirms the task exists
            _subscribedTaskId = row.Id;

            // Replay the latest run's persisted log so output is visible across app restarts.
            await ReplayLogFileAsync(entity.LogPath, ct);

            var subs = await subtaskRepo.GetByTaskIdAsync(row.Id);
            ct.ThrowIfCancellationRequested();
            foreach (var s in subs)
                Subtasks.Add(new SubtaskRowViewModel { Id = s.Id, Title = s.Title, Done = s.Completed });

            if (entity.PlanningPhase != ClaudeDo.Data.Models.PlanningPhase.None)
            {
                await LoadPlanningChildrenAsync(row.Id, ct);
            }
        }
        catch (OperationCanceledException) { }
    }

    private async System.Threading.Tasks.Task ReplayLogFileAsync(string? logPath, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(logPath)) return;
        var expanded = ExpandUserPath(logPath);
        if (!System.IO.File.Exists(expanded)) return;

        try
        {
            const int maxLines = 2000;
            string[] all;
            await using (var fs = new System.IO.FileStream(
                             expanded,
                             System.IO.FileMode.Open,
                             System.IO.FileAccess.Read,
                             System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete))
            using (var reader = new System.IO.StreamReader(fs))
            {
                var list = new List<string>();
                while (await reader.ReadLineAsync(ct) is { } line)
                    list.Add(line);
                all = list.ToArray();
            }
            ct.ThrowIfCancellationRequested();

            var start = Math.Max(0, all.Length - maxLines);
            for (int i = start; i < all.Length; i++)
            {
                ct.ThrowIfCancellationRequested();
                if (_subscribedTaskId is null) return;
                // Worker writes raw Claude CLI stdout to disk (no prefix) but broadcasts
                // it with a "[stdout] " prefix. Match the live-stream format so the same
                // stream-json parser handles both.
                var line = all[i];
                var normalized = line.StartsWith("[", StringComparison.Ordinal) ? line : "[stdout] " + line;
                OnTaskMessage(_subscribedTaskId, normalized);
            }
            FlushClaudeBuffer();
        }
        catch (OperationCanceledException) { throw; }
        catch { /* best-effort replay */ }
    }

    private static string ExpandUserPath(string path)
    {
        if (path.StartsWith("~/", StringComparison.Ordinal) || path.StartsWith("~\\", StringComparison.Ordinal))
            return System.IO.Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
                path[2..]);
        if (path == "~")
            return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
        return path;
    }

    private async System.Threading.Tasks.Task LoadPlanningChildrenAsync(string parentTaskId, CancellationToken ct)
    {
        try
        {
            await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
            var children = await ctx.Tasks
                .AsNoTracking()
                .Include(t => t.Worktree)
                .Where(t => t.ParentTaskId == parentTaskId)
                .ToListAsync(ct);
            ct.ThrowIfCancellationRequested();

            foreach (var child in children)
            {
                var existing = Subtasks.FirstOrDefault(s => s.Id == child.Id);
                if (existing != null)
                {
                    existing.Status = child.Status;
                    existing.WorktreeState = child.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active;
                }
            }

            if (MergeTargetBranches.Count == 0)
            {
                var childWithWorktree = children.FirstOrDefault(c => c.Worktree != null);
                if (childWithWorktree != null)
                {
                    var targets = await _worker.GetMergeTargetsAsync(childWithWorktree.Id);
                    if (targets != null)
                    {
                        MergeTargetBranches.Clear();
                        foreach (var b in targets.LocalBranches)
                            MergeTargetBranches.Add(b);
                        SelectedMergeTarget = targets.DefaultBranch;
                    }
                }
            }

            RecomputeCanMergeAll();
        }
        catch (OperationCanceledException) { }
        catch { /* best-effort */ }
    }

    private async System.Threading.Tasks.Task RefreshPlanningChildAsync(string childTaskId)
    {
        if (Task is null) return;
        try
        {
            await using var ctx = await _dbFactory.CreateDbContextAsync();
            var child = await ctx.Tasks
                .AsNoTracking()
                .Include(t => t.Worktree)
                .FirstOrDefaultAsync(t => t.Id == childTaskId && t.ParentTaskId == Task.Id);
            if (child == null) return;

            var existing = Subtasks.FirstOrDefault(s => s.Id == child.Id);
            if (existing != null)
            {
                existing.Status = child.Status;
                existing.WorktreeState = child.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active;
            }

            RecomputeCanMergeAll();
        }
        catch { /* best-effort */ }
    }

    internal void RecomputeCanMergeAll()
    {
        var notDone = Subtasks.Count(c => c.Status != ClaudeDo.Data.Models.TaskStatus.Done);
        if (notDone > 0)
        {
            CanMergeAll = false;
            MergeAllDisabledReason = $"{notDone} subtask(s) not done";
            return;
        }
        var badWt = Subtasks.FirstOrDefault(c =>
            c.WorktreeState == ClaudeDo.Data.Models.WorktreeState.Discarded ||
            c.WorktreeState == ClaudeDo.Data.Models.WorktreeState.Kept);
        if (badWt is not null)
        {
            CanMergeAll = false;
            MergeAllDisabledReason = "at least one worktree was discarded/kept";
            return;
        }
        CanMergeAll = true;
        MergeAllDisabledReason = null;
    }

    [RelayCommand(CanExecute = nameof(CanReviewDiff))]
    private async System.Threading.Tasks.Task ReviewCombinedDiffAsync()
    {
        if (Task is null || ShowPlanningDiffModal is null) return;
        var vm = new ViewModels.Planning.PlanningDiffViewModel(_worker, Task.Id, SelectedMergeTarget ?? "main");
        await vm.InitializeAsync();
        await ShowPlanningDiffModal(vm);
    }

    private bool CanReviewDiff() => Task?.IsPlanningParent == true && Subtasks.Any();

    [RelayCommand(CanExecute = nameof(CanMergeAll))]
    private async System.Threading.Tasks.Task MergeAllAsync()
    {
        MergeAllError = null;
        try
        {
            await _worker.MergeAllPlanningAsync(Task!.Id, SelectedMergeTarget ?? "main");
        }
        catch (Exception ex)
        {
            MergeAllError = ex.Message;
        }
    }

    private async System.Threading.Tasks.Task RefreshWorktreeAsync(string taskId)
    {
        try
        {
            await using var ctx = await _dbFactory.CreateDbContextAsync();
            var entity = await ctx.Tasks
                .AsNoTracking()
                .Include(t => t.Worktree)
                .FirstOrDefaultAsync(t => t.Id == taskId);
            if (entity == null || Task?.Id != taskId) return;

            WorktreePath = entity.Worktree?.Path;
            WorktreeBaseCommit = entity.Worktree?.BaseCommit;
            WorktreeStateLabel = entity.Worktree?.State.ToString();
            BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
            AgentStatusLabel = entity.Status.ToString();
            if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
                row.DiffStat = stat;
        }
        catch { /* best-effort refresh */ }
    }

    [RelayCommand(CanExecute = nameof(CanOpenDiff))]
    private async System.Threading.Tasks.Task OpenDiffAsync()
    {
        if (WorktreePath == null || ShowDiffModal == null) return;
        var diffVm = new DiffModalViewModel(_services.GetRequiredService<ClaudeDo.Data.Git.GitService>())
        {
            WorktreePath = WorktreePath,
            BaseRef = WorktreeBaseCommit,
            TaskId = Task?.Id,
            TaskTitle = Task?.Title ?? "",
            ShowMergeModal = ShowMergeModal,
            ResolveMergeVm = () => _services.GetRequiredService<MergeModalViewModel>(),
        };
        await diffVm.LoadAsync();
        await ShowDiffModal(diffVm);
    }

    private bool CanOpenDiff() => WorktreePath != null;

    [RelayCommand(CanExecute = nameof(CanOpenWorktree))]
    private void OpenWorktree()
    {
        if (WorktreePath == null) return;
        try
        {
            System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
            {
                FileName = WorktreePath,
                UseShellExecute = true,
            });
        }
        catch { /* explorer open is best-effort */ }
    }

    private bool CanOpenWorktree() => WorktreePath != null;

    partial void OnWorktreePathChanged(string? value)
    {
        OpenDiffCommand.NotifyCanExecuteChanged();
        OpenWorktreeCommand.NotifyCanExecuteChanged();
    }

    [RelayCommand]
    private void CloseDetails() => CloseDetail?.Invoke();

    [RelayCommand]
    private async System.Threading.Tasks.Task ToggleStarAsync()
    {
        if (Task is null) return;
        Task.IsStarred = !Task.IsStarred;
        await using var ctx = _dbFactory.CreateDbContext();
        var repo = new TaskRepository(ctx);
        var entity = await repo.GetByIdAsync(Task.Id);
        if (entity is null) return;
        entity.IsStarred = Task.IsStarred;
        await repo.UpdateAsync(entity);
    }

    [RelayCommand]
    private async System.Threading.Tasks.Task ToggleDoneAsync()
    {
        if (Task is null) return;
        Task.Done = !Task.Done;
        await using var ctx = _dbFactory.CreateDbContext();
        var repo = new TaskRepository(ctx);
        var entity = await repo.GetByIdAsync(Task.Id);
        if (entity is null) return;
        entity.Status = Task.Done
            ? ClaudeDo.Data.Models.TaskStatus.Done
            : ClaudeDo.Data.Models.TaskStatus.Idle;
        Task.Status = entity.Status;
        AgentStatusLabel = entity.Status.ToString();
        await repo.UpdateAsync(entity);
    }

    [RelayCommand]
    private async System.Threading.Tasks.Task ToggleSubtaskDoneAsync(SubtaskRowViewModel? row)
    {
        if (row is null) return;
        row.Done = !row.Done;
        await using var ctx = _dbFactory.CreateDbContext();
        var repo = new SubtaskRepository(ctx);
        var subs = await repo.GetByTaskIdAsync(Task?.Id ?? "");
        var entity = subs.FirstOrDefault(s => s.Id == row.Id);
        if (entity is null) return;
        entity.Completed = row.Done;
        await repo.UpdateAsync(entity);
    }

    [RelayCommand]
    private async System.Threading.Tasks.Task DeleteTaskAsync()
    {
        if (Task == null) return;
        var row = Task;
        if (ConfirmAsync != null)
        {
            var ok = await ConfirmAsync($"Delete \"{row.Title}\"? This cannot be undone.");
            if (!ok) return;
        }
        try
        {
            await using var ctx = _dbFactory.CreateDbContext();
            var repo = new TaskRepository(ctx);
            await repo.DeleteAsync(row.Id);
        }
        catch (DbUpdateException ex) when (
               ex.Message.Contains("FOREIGN KEY", StringComparison.OrdinalIgnoreCase)
            || ex.InnerException?.Message.Contains("FOREIGN KEY", StringComparison.OrdinalIgnoreCase) == true)
        {
            if (ShowErrorAsync != null)
                await ShowErrorAsync("This task has child tasks. Discard the planning session or delete child tasks first.");
            return;
        }
        if (DeleteFromList != null)
            await DeleteFromList(row);
        CloseDetail?.Invoke();
    }

    [RelayCommand]
    private async System.Threading.Tasks.Task AddSubtaskAsync()
    {
        if (Task is null) return;
        var title = NewSubtaskTitle?.Trim();
        if (string.IsNullOrEmpty(title)) return;

        var entity = new ClaudeDo.Data.Models.SubtaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            TaskId = Task.Id,
            Title = title,
            Completed = false,
            OrderNum = Subtasks.Count,
            CreatedAt = DateTime.UtcNow,
        };

        await using var ctx = _dbFactory.CreateDbContext();
        await new SubtaskRepository(ctx).AddAsync(entity);

        Subtasks.Add(new SubtaskRowViewModel { Id = entity.Id, Title = entity.Title, Done = entity.Completed });
        NewSubtaskTitle = "";
    }

    [RelayCommand]
    private async System.Threading.Tasks.Task StopAsync()
    {
        if (Task == null) return;
        await _worker.CancelTaskAsync(Task.Id);
    }

    [RelayCommand(CanExecute = nameof(CanEnqueue))]
    private async System.Threading.Tasks.Task EnqueueAsync()
    {
        if (Task == null) return;
        try
        {
            await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Queued);
            AgentStatusLabel = "Queued";
        }
        catch { /* offline */ }
    }

    private bool CanEnqueue() =>
        Task != null && _worker.IsConnected && IsIdle
        && (!Task.IsChild || Task.ParentFinalized);

    [RelayCommand(CanExecute = nameof(CanDequeue))]
    private async System.Threading.Tasks.Task DequeueAsync()
    {
        if (Task == null) return;
        try
        {
            await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Idle);
            AgentStatusLabel = "Idle";
        }
        catch { /* offline */ }
    }

    private bool CanDequeue() =>
        Task != null && _worker.IsConnected && IsQueued;

    [RelayCommand(CanExecute = nameof(CanContinue))]
    private async System.Threading.Tasks.Task ContinueAsync()
    {
        if (Task == null) return;
        await _worker.ContinueTaskAsync(Task.Id, "Continue working on this task.");
    }

    private bool CanContinue() =>
        Task != null && _worker.IsConnected && ShowContinue && !string.IsNullOrEmpty(LatestRunSessionId);

    [RelayCommand(CanExecute = nameof(CanResetAndRetry))]
    private async System.Threading.Tasks.Task ResetAndRetryAsync()
    {
        if (Task == null) return;
        if (ConfirmAsync == null) return;

        var branchName = $"claudedo/{Task.Id.Replace("-", "")}";
        var ok = await ConfirmAsync(
            $"Reset and retry?\nThis discards branch {branchName} (and uncommitted changes), then queues the task to run from the beginning.");
        if (!ok) return;

        if (WorktreePath != null)
            await _worker.ResetTaskAsync(Task.Id);

        try
        {
            await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Queued);
            AgentStatusLabel = "Queued";
        }
        catch { /* offline */ }
    }

    private bool CanResetAndRetry() =>
        Task != null && _worker.IsConnected && ShowResetAndRetry;
}

public sealed partial class SubtaskRowViewModel : ViewModelBase
{
    public required string Id { get; init; }
    [ObservableProperty] private string _title = "";
    [ObservableProperty] private bool _done;
    [ObservableProperty] private ClaudeDo.Data.Models.TaskStatus _status;
    [ObservableProperty] private ClaudeDo.Data.Models.WorktreeState _worktreeState = ClaudeDo.Data.Models.WorktreeState.Active;
}
