using CommunityToolkit.Mvvm.ComponentModel;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Localization;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Ui.ViewModels.Islands;

public sealed partial class TaskRowViewModel : ViewModelBase
{
    public required string Id { get; init; }
    [ObservableProperty] private int _number;
    [ObservableProperty] private string _title = "";
    [ObservableProperty] private string _listName = "";
    [ObservableProperty] private bool _done;
    [ObservableProperty] private bool _isStarred;
    [ObservableProperty] private bool _isMyDay;
    [ObservableProperty] private bool _isSelected;
    [ObservableProperty] private TaskStatus _status;
    [ObservableProperty] private PlanningPhase _planningPhase;
    [ObservableProperty] private string? _branch;
    [ObservableProperty] private string? _diffStat;
    [ObservableProperty] private ClaudeDo.Data.Models.WorktreeState? _worktreeState;
    [ObservableProperty] private string? _worktreePath;
    [ObservableProperty] private DateTime? _scheduledFor;
    [ObservableProperty] private int _diffAdditions;
    [ObservableProperty] private int _diffDeletions;
    [ObservableProperty] private bool _dropHintAbove;
    [ObservableProperty] private bool _dropHintBelow;
    [ObservableProperty] private string? _parentTaskId;
    [ObservableProperty] private string? _createdBy;
    [ObservableProperty] private string? _blockedByTaskId;
    [ObservableProperty] private string? _dependsOnTaskId;
    [ObservableProperty] private bool _isExpanded = true;
    [ObservableProperty] private bool _hasPlanningChildren;
    [ObservableProperty] private bool _hasQueuedSubtasks;
    [ObservableProperty] private bool _showListChip = true;
    [ObservableProperty] private bool _parentFinalized;
    [ObservableProperty] private bool _parentInView = true;
    // The three below are extrinsic — computed and assigned by TasksIslandViewModel.Regroup
    // (it alone has the cross-row graph needed to walk DependsOnTaskId chains), mirroring how
    // ParentInView is assigned by the same pass rather than derived locally.
    [ObservableProperty] private bool _showAsChainMember;
    [ObservableProperty] private int? _chainStep;
    [ObservableProperty] private string? _chainAfterLabel;
    [ObservableProperty] private int _roadblockCount;
    // Only meaningful when Status=Failed; stamped by TaskRunner.MarkFailed. "unknown" for a
    // Failed task that predates this field. Null on every other status.
    [ObservableProperty] private string? _failureReason;
    [ObservableProperty] private int? _failureTurnsUsed;
    [ObservableProperty] private int? _failureMaxTurns;
    [ObservableProperty] private bool _isRefining;
    // Manual = a reminder only the user can do. Every "hand this to Claude" affordance is hidden
    // and automation skips it; opening a hand-driven ConPTY session stays allowed.
    [ObservableProperty] private bool _isManual;
    // Set by the shell from Mission Control's open ConPTY panes: this task has a live hand-driven
    // session, which outranks the persisted status on the lifecycle chip (an interactive task is
    // typically Idle+Active-worktree, i.e. would otherwise read "Parked").
    [ObservableProperty] private bool _hasInteractiveSession;
    // Set by the custom drag while this row is being dragged — drives the "grabbed" row style.
    [ObservableProperty] private bool _isDragging;

    // Set from PointerEntered/PointerExited in TaskRowView.axaml.cs. Drives ShowRowActions
    // together with IsSelected, so hover-only affordances (refine button, unset star, dequeue-X)
    // stay reachable via keyboard/selection, not just mouse hover.
    [ObservableProperty] private bool _isHovered;

    // Transient: set from HubBroadcaster's OperationProgress while the worker is still creating
    // this task's worktree (the silent gap between Queued and the first agent output). Cleared
    // by the next entity refresh — UpdateFromEntity always reflects a settled state, so there's
    // nothing left to show past that point.
    [ObservableProperty] private string? _creationPhase;

    // True while a drag is hovering this row (i.e. it would show a drop-hint gap). Used to
    // suppress the ordinary hover highlight/transitions so they don't fight the hint.
    public bool IsDropTarget => DropHintAbove || DropHintBelow;

    // Hover-or-selected gate for the row's optional action affordances (refine button, unset
    // star, dequeue-X) — a set star and the IsRefining spinner are exempt, they show always.
    public bool ShowRowActions => IsHovered || IsSelected;

    public string? ManualBadge => IsManual ? Loc.T("tasks.badgeManual") : null;

    public DateTime CreatedAt { get; init; }
    public string CreatedAtFormatted => CreatedAt == default ? "—" : Loc.T("vm.taskRow.createdPrefix", CreatedAt.ToString("MMM d"));

    public int StepsCount { get; init; }
    public int StepsCompleted { get; init; }

    // Number is 0 for rows created outside TaskNumberAllocator (test seeds, a future import
    // path); a bare "#0" would be meaningless, so hide the badge entirely below 1.
    public bool ShowNumberBadge   => Number > 0;

    public bool IsChild            => !string.IsNullOrEmpty(ParentTaskId);
    public bool IsAgentSuggested   => IsChild && !string.IsNullOrEmpty(CreatedBy) && CreatedBy == ParentTaskId;
    public bool IsPlanningParent => PlanningPhase != PlanningPhase.None
                                  || HasPlanningChildren;
    // A child only reads as a child while its parent shares the current view. When the parent is
    // absent (removed from My Day, or daily-prep placed a lone child there), the row renders as a
    // normal top-level task instead of an orphaned, indented Draft.
    public bool ShowAsChild      => IsChild && ParentInView;
    // A subtask is Draft until its planning parent is finalized, then Planned (queueable).
    public bool IsDraft          => ShowAsChild && Status == TaskStatus.Idle && !ParentFinalized;
    public bool IsPlanned        => ShowAsChild && Status == TaskStatus.Idle && ParentFinalized;

    public bool CanOpenPlanningSession   => Status == TaskStatus.Idle
                                          && PlanningPhase == PlanningPhase.None
                                          && !IsChild
                                          && !IsManual;
    public bool CanResumeOrDiscardPlanning => PlanningPhase == PlanningPhase.Active;

    public string? PlanningBadge => PlanningPhase switch
    {
        PlanningPhase.Active    => Loc.T("vm.planningBadge.active"),
        PlanningPhase.Finalized => Loc.T("vm.planningBadge.finalized"),
        _                       => null,
    };

    public bool IsPlanActive    => PlanningPhase == PlanningPhase.Active;
    public bool IsPlanFinalized => PlanningPhase == PlanningPhase.Finalized;

    public bool HasBranch   => !string.IsNullOrWhiteSpace(Branch);
    public bool HasDiff     => DiffAdditions > 0 || DiffDeletions > 0;
    public bool HasSteps    => StepsCount > 0;
    public bool IsOverdue   => ScheduledFor is { } d && d.ToLocalTime().Date < DateTime.Today && !Done;
    public bool IsRunning   => Status == TaskStatus.Running;
    public bool IsWaitingForReview => Status == TaskStatus.WaitingForReview;
    // Parked = set aside from review: Idle but still holding its Active worktree (vs a plain Idle
    // task). A live ConPTY session is that same shape but not parked — it reads "Interactive".
    public bool IsParked    => Status == TaskStatus.Idle && WorktreeState == ClaudeDo.Data.Models.WorktreeState.Active
                               && !HasInteractiveSession;
    public bool IsQueued    => Status == TaskStatus.Queued && string.IsNullOrEmpty(BlockedByTaskId);
    public bool IsWaiting   => Status == TaskStatus.Queued && !string.IsNullOrEmpty(BlockedByTaskId);
    public bool CanRemoveFromQueue => IsQueued || HasQueuedSubtasks;
    // "Send to queue" is the single queue entry. On a finalized planning parent it queues the
    // plan (children) via CanQueuePlan; an Active (not-yet-finalized) planning parent is hidden —
    // it must be finalized first. The worker never sees a UI-hosted ConPTY session (it never
    // touches task status), so this gate has to live here: queueing a task the user is actively
    // hand-editing in an interactive pane would spawn an autonomous run racing it in the same
    // worktree.
    public bool CanSendToQueue => SendToQueueGate().Can;
    // Parent-level "send plan to queue" — only once the plan is finalized (children Planned).
    // Drives the routing inside SendToQueue, not a separate menu entry.
    public bool CanQueuePlan => !IsChild && HasPlanningChildren
                                && PlanningPhase == PlanningPhase.Finalized
                                && !HasQueuedSubtasks;
    // User-triggered finalize for a planning parent whose session was closed before finalizing.
    public bool CanFinalizePlanning => PlanningPhase == PlanningPhase.Active && !IsChild;

    // Context-menu Group 1 reasons: null when the paired CanX is true, otherwise the specific
    // blocking condition — shown as a disabled MenuItem's tooltip (ToolTip.ShowOnDisabled="True",
    // verified headlessly to actually render on Avalonia 12 — see task A3). Each pair (CanX,
    // XDisabledReason) is derived from one private "gate" method so there is a single source of
    // truth for the condition instead of a CanX expression and a hand-written negation of it.
    private (bool Can, string? Reason) SendToQueueGate()
    {
        if (IsRunning) return (false, Loc.T("tasks.reasonAlreadyRunning"));
        if (IsQueued) return (false, Loc.T("tasks.reasonAlreadyQueued"));
        if (IsWaitingForReview) return (false, Loc.T("tasks.reasonWaitingForReview"));
        if (HasQueuedSubtasks) return (false, Loc.T("tasks.reasonSubtasksQueued"));
        if (IsChild && !ParentFinalized) return (false, Loc.T("tasks.reasonPlanNotFinalized"));
        if (PlanningPhase == PlanningPhase.Active) return (false, Loc.T("tasks.reasonPlanningActive"));
        if (IsManual) return (false, Loc.T("tasks.manualTip"));
        if (HasInteractiveSession) return (false, Loc.T("tasks.reasonInteractiveSession"));
        return (true, null);
    }
    public string? SendToQueueDisabledReason => SendToQueueGate().Reason;

    // IsRunning is a standalone status property used well beyond this one gate, so it stays as
    // is; only the reason text is derived from a gate method for consistency with the others.
    private (bool Can, string? Reason) CancelGate() =>
        IsRunning ? (true, null) : (false, Loc.T("tasks.reasonNotRunning"));
    public string? CancelDisabledReason => CancelGate().Reason;

    // Gates "Mark Done"/"Mark Cancelled" in the context menu — forcing either status while the
    // task is actively running would strand the CLI process untracked (WorkerHub.SetTaskStatus
    // rejects it server-side too; this is the UI-visible half of that guard).
    private (bool Can, string? Reason) MarkAsGate() =>
        IsRunning ? (false, Loc.T("tasks.reasonAlreadyRunning")) : (true, null);
    public string? MarkAsDisabledReason => MarkAsGate().Reason;

    // "Open quick session" has no precondition today (see A1's file header) — kept as a
    // placeholder so the menu wiring is uniform; always null until a real gate exists.
    public string? QuickSessionDisabledReason => null;

    // The row keeps a merged/discarded worktree's recorded Path, so a non-empty string is not
    // proof the folder is still there — same on-disk check the worktrees overview and the merge
    // section do before offering "open". Only read when the context menu is built, not per row;
    // each call to the gate does exactly one Directory.Exists (no caching).
    private (bool Can, string? Reason) OpenWorktreeGate()
    {
        var can = !string.IsNullOrWhiteSpace(WorktreePath) && System.IO.Directory.Exists(WorktreePath);
        return (can, can ? null : Loc.T("tasks.reasonNoWorktree"));
    }
    public bool CanOpenWorktree => OpenWorktreeGate().Can;
    public string? OpenWorktreeDisabledReason => OpenWorktreeGate().Reason;

    private (bool Can, string? Reason) RefineGate()
    {
        if (IsManual) return (false, Loc.T("tasks.manualTip"));
        if (IsRefining) return (false, Loc.T("tasks.reasonRefining"));
        if (PlanningPhase != PlanningPhase.None) return (false, Loc.T("tasks.reasonIsPlanningParent"));
        if (Status != TaskStatus.Idle) return (false, Loc.T("tasks.reasonNotIdle"));
        return (true, null);
    }
    public bool CanRefine => RefineGate().Can;
    public string? RefineDisabledReason => RefineGate().Reason;

    // Gates the "Planning" submenu header — null unless Open/Resume-or-Discard/Finalize would
    // all be hidden, i.e. the submenu would otherwise open empty. The three CanX below stay
    // standalone properties (bindings/tests use them individually); the gate only derives the
    // combined reason.
    private (bool Can, string? Reason) PlanningGate()
    {
        if (CanOpenPlanningSession || CanResumeOrDiscardPlanning || CanFinalizePlanning) return (true, null);
        if (IsManual) return (false, Loc.T("tasks.manualTip"));
        if (IsChild) return (false, Loc.T("tasks.reasonIsChildTask"));
        if (PlanningPhase == PlanningPhase.Finalized) return (false, Loc.T("tasks.reasonPlanAlreadyFinalized"));
        return (false, Loc.T("tasks.reasonNotIdle"));
    }
    public string? PlanningDisabledReason => PlanningGate().Reason;

    // Deleting out from under a live ConPTY session would orphan the claude process in Mission
    // Control (it never gets a task-deleted signal) — same rationale as SendToQueueGate.
    private (bool Can, string? Reason) DeleteGate() =>
        HasInteractiveSession ? (false, Loc.T("tasks.reasonInteractiveSession")) : (true, null);
    public bool CanDeleteTask => DeleteGate().Can;
    public string? DeleteDisabledReason => DeleteGate().Reason;

    public bool HasSchedule => ScheduledFor.HasValue;
    // "Add to My Day" — shown on any task not already in My Day; a Done task has no place in
    // today's focus list. The mirror of "Remove from My Day" (gated on IsMyDay).
    public bool CanAddToMyDay => !IsMyDay && !Done;
    public bool HasRoadblock => RoadblockCount > 0;
    public string RoadblockTooltip => RoadblockCount == 1
        ? "1 roadblock reported during the run — see details"
        : $"{RoadblockCount} roadblocks reported during the run — see details";

    /// Mirrors WorktreeManager.PhaseCreatingWorktree — a hub payload token, not a display string.
    private const string PhaseCreatingWorktree = "creating_worktree";

    public bool HasCreationPhase => CreationPhase is not null;
    public string? CreationPhaseLabel => CreationPhase switch
    {
        PhaseCreatingWorktree => Loc.T("ops.worker.creatingWorktree"),
        _ => null,
    };

    // True for every Failed task, even one that predates this field — FailureReasonLabel then
    // falls back to "unknown" instead of leaving the tooltip blank.
    public bool HasFailureReason => Status == TaskStatus.Failed;
    public string FailureReasonLabel => FailureReason switch
    {
        "max_turns"   => Loc.T("vm.failureReason.maxTurns"),
        "timeout"     => Loc.T("vm.failureReason.timeout"),
        "usage_limit" => Loc.T("vm.failureReason.usageLimit"),
        "cancelled"   => Loc.T("vm.failureReason.cancelled"),
        "error"       => Loc.T("vm.failureReason.error"),
        _             => Loc.T("vm.failureReason.unknown"),
    };
    // max_turns gets the actionable detail (turns used/configured) since that's the one case
    // where the fix is "raise maxTurns and continue_task", not "reset and re-run".
    public string? FailureReasonTooltip => !HasFailureReason ? null
        : FailureReason == "max_turns" && FailureTurnsUsed is { } used && FailureMaxTurns is { } max
            ? Loc.T("vm.failureReasonTooltip.maxTurns", used, max)
            : FailureReasonLabel;

    // Drives the status chip tooltip: an open interactive session takes priority (it's tappable),
    // otherwise a Failed task's reason, otherwise no tooltip.
    public string? StatusChipTooltip
        => HasInteractiveSession ? Loc.T("tasks.interactiveChipTip") : FailureReasonTooltip;

    public string DiffAdditionsText => $"+{DiffAdditions}";
    public string DiffDeletionsText => $"−{DiffDeletions}";
    public string StepsText         => Loc.T("vm.taskRow.stepsText", StepsCompleted, StepsCount);

    public string StatusLabel
        => HasInteractiveSession ? Loc.T("vm.taskStatus.interactive")
         : IsParked ? Loc.T("vm.taskStatus.parked") : Status switch
    {
        TaskStatus.Idle             => Loc.T("vm.taskStatus.idle"),
        TaskStatus.Queued           => Loc.T("vm.taskStatus.queued"),
        TaskStatus.Running          => Loc.T("vm.taskStatus.running"),
        TaskStatus.WaitingForReview    => Loc.T("vm.taskStatus.waitingForReview"),
        TaskStatus.WaitingForChildren  => Loc.T("vm.taskStatus.waitingForChildren"),
        TaskStatus.Done             => Loc.T("vm.taskStatus.done"),
        TaskStatus.Failed           => Loc.T("vm.taskStatus.failed"),
        TaskStatus.Cancelled        => Loc.T("vm.taskStatus.cancelled"),
        _                           => Status.ToString(),
    };

    // A planning parent that is still Idle (Active planning, or Finalized-but-not-yet-queued)
    // already shows its state via the PLANNING/PLANNED badge next to the title. The lifecycle
    // chip would read "Idle" (or "Parked"), which is misleading, so hide it in that case —
    // unless there's a live session, where "Interactive" is exactly what the user needs to see.
    public bool ShowStatusChip => HasInteractiveSession
                                  || !(PlanningPhase != PlanningPhase.None && Status == TaskStatus.Idle);

    public string StatusChipClass => HasInteractiveSession
        ? "interactive"
        : (Status, IsBlocked: !string.IsNullOrEmpty(BlockedByTaskId)) switch
    {
        (TaskStatus.Running,            _) => "running",
        (TaskStatus.WaitingForReview,   _) => "review",
        (TaskStatus.WaitingForChildren, _) => "children",
        (TaskStatus.Failed,           _) => "error",
        (TaskStatus.Done,             _) => "done",
        (TaskStatus.Queued,        true) => "waiting",
        (TaskStatus.Queued,       false) => "queued",
        _                                => "idle",
    };

    partial void OnStatusChanged(TaskStatus value)
    {
        OnPropertyChanged(nameof(StatusChipClass));
        OnPropertyChanged(nameof(StatusLabel));
        OnPropertyChanged(nameof(ShowStatusChip));
        OnPropertyChanged(nameof(IsRunning));
        OnPropertyChanged(nameof(IsWaitingForReview));
        OnPropertyChanged(nameof(IsParked));
        OnPropertyChanged(nameof(IsQueued));
        OnPropertyChanged(nameof(IsWaiting));
        OnPropertyChanged(nameof(HasFailureReason));
        OnPropertyChanged(nameof(FailureReasonTooltip));
        OnPropertyChanged(nameof(StatusChipTooltip));
        OnPropertyChanged(nameof(IsDraft));
        OnPropertyChanged(nameof(IsPlanned));
        OnPropertyChanged(nameof(CanOpenPlanningSession));
        OnPropertyChanged(nameof(CanRemoveFromQueue));
        OnPropertyChanged(nameof(CanSendToQueue));
        OnPropertyChanged(nameof(CanRefine));
        OnPropertyChanged(nameof(SendToQueueDisabledReason));
        OnPropertyChanged(nameof(CancelDisabledReason));
        OnPropertyChanged(nameof(RefineDisabledReason));
        OnPropertyChanged(nameof(PlanningDisabledReason));
        OnPropertyChanged(nameof(MarkAsDisabledReason));
    }

    partial void OnParentTaskIdChanged(string? value)
    {
        OnPropertyChanged(nameof(IsChild));
        OnPropertyChanged(nameof(IsAgentSuggested));
        OnPropertyChanged(nameof(ShowAsChild));
        OnPropertyChanged(nameof(IsDraft));
        OnPropertyChanged(nameof(IsPlanned));
        OnPropertyChanged(nameof(CanSendToQueue));
        OnPropertyChanged(nameof(CanOpenPlanningSession));
        OnPropertyChanged(nameof(SendToQueueDisabledReason));
        OnPropertyChanged(nameof(PlanningDisabledReason));
    }

    partial void OnParentInViewChanged(bool value)
    {
        OnPropertyChanged(nameof(ShowAsChild));
        OnPropertyChanged(nameof(IsDraft));
        OnPropertyChanged(nameof(IsPlanned));
    }

    partial void OnCreatedByChanged(string? value) => OnPropertyChanged(nameof(IsAgentSuggested));

    partial void OnParentFinalizedChanged(bool value)
    {
        OnPropertyChanged(nameof(IsDraft));
        OnPropertyChanged(nameof(IsPlanned));
        OnPropertyChanged(nameof(CanSendToQueue));
        OnPropertyChanged(nameof(SendToQueueDisabledReason));
    }

    partial void OnPlanningPhaseChanged(PlanningPhase value)
    {
        OnPropertyChanged(nameof(IsPlanningParent));
        OnPropertyChanged(nameof(PlanningBadge));
        OnPropertyChanged(nameof(ShowStatusChip));
        OnPropertyChanged(nameof(IsPlanActive));
        OnPropertyChanged(nameof(IsPlanFinalized));
        OnPropertyChanged(nameof(CanOpenPlanningSession));
        OnPropertyChanged(nameof(CanResumeOrDiscardPlanning));
        OnPropertyChanged(nameof(CanQueuePlan));
        OnPropertyChanged(nameof(CanSendToQueue));
        OnPropertyChanged(nameof(CanFinalizePlanning));
        OnPropertyChanged(nameof(CanRefine));
        OnPropertyChanged(nameof(SendToQueueDisabledReason));
        OnPropertyChanged(nameof(RefineDisabledReason));
        OnPropertyChanged(nameof(PlanningDisabledReason));
    }

    partial void OnIsRefiningChanged(bool value)
    {
        OnPropertyChanged(nameof(CanRefine));
        OnPropertyChanged(nameof(RefineDisabledReason));
    }

    partial void OnIsManualChanged(bool value)
    {
        OnPropertyChanged(nameof(ManualBadge));
        OnPropertyChanged(nameof(CanRefine));
        OnPropertyChanged(nameof(CanSendToQueue));
        OnPropertyChanged(nameof(CanOpenPlanningSession));
        OnPropertyChanged(nameof(SendToQueueDisabledReason));
        OnPropertyChanged(nameof(RefineDisabledReason));
        OnPropertyChanged(nameof(PlanningDisabledReason));
    }

    partial void OnHasInteractiveSessionChanged(bool value)
    {
        OnPropertyChanged(nameof(IsParked));
        OnPropertyChanged(nameof(StatusChipClass));
        OnPropertyChanged(nameof(StatusLabel));
        OnPropertyChanged(nameof(ShowStatusChip));
        OnPropertyChanged(nameof(StatusChipTooltip));
        OnPropertyChanged(nameof(CanSendToQueue));
        OnPropertyChanged(nameof(SendToQueueDisabledReason));
        OnPropertyChanged(nameof(CanDeleteTask));
        OnPropertyChanged(nameof(DeleteDisabledReason));
    }

    partial void OnFailureReasonChanged(string? value)
    {
        OnPropertyChanged(nameof(HasFailureReason));
        OnPropertyChanged(nameof(FailureReasonLabel));
        OnPropertyChanged(nameof(FailureReasonTooltip));
        OnPropertyChanged(nameof(StatusChipTooltip));
    }

    partial void OnFailureTurnsUsedChanged(int? value) => OnPropertyChanged(nameof(FailureReasonTooltip));
    partial void OnFailureMaxTurnsChanged(int? value) => OnPropertyChanged(nameof(FailureReasonTooltip));

    partial void OnHasQueuedSubtasksChanged(bool value)
    {
        OnPropertyChanged(nameof(CanRemoveFromQueue));
        OnPropertyChanged(nameof(CanSendToQueue));
        OnPropertyChanged(nameof(CanQueuePlan));
        OnPropertyChanged(nameof(SendToQueueDisabledReason));
    }

    partial void OnIsHoveredChanged(bool value) => OnPropertyChanged(nameof(ShowRowActions));
    partial void OnIsSelectedChanged(bool value) => OnPropertyChanged(nameof(ShowRowActions));

    partial void OnBlockedByTaskIdChanged(string? value)
    {
        OnPropertyChanged(nameof(IsQueued));
        OnPropertyChanged(nameof(IsWaiting));
        OnPropertyChanged(nameof(StatusChipClass));
    }

    partial void OnHasPlanningChildrenChanged(bool value)
    {
        OnPropertyChanged(nameof(IsPlanningParent));
        OnPropertyChanged(nameof(CanQueuePlan));
    }

    partial void OnNumberChanged(int value)        => OnPropertyChanged(nameof(ShowNumberBadge));
    partial void OnBranchChanged(string? value)    => OnPropertyChanged(nameof(HasBranch));
    partial void OnWorktreePathChanged(string? value)
    {
        OnPropertyChanged(nameof(CanOpenWorktree));
        OnPropertyChanged(nameof(OpenWorktreeDisabledReason));
    }
    partial void OnWorktreeStateChanged(ClaudeDo.Data.Models.WorktreeState? value)
    {
        OnPropertyChanged(nameof(IsParked));
        OnPropertyChanged(nameof(StatusLabel));
    }
    partial void OnDoneChanged(bool value)
    {
        OnPropertyChanged(nameof(IsOverdue));
        OnPropertyChanged(nameof(CanAddToMyDay));
    }
    partial void OnIsMyDayChanged(bool value)      => OnPropertyChanged(nameof(CanAddToMyDay));
    partial void OnScheduledForChanged(DateTime? value)
    {
        OnPropertyChanged(nameof(IsOverdue));
        OnPropertyChanged(nameof(HasSchedule));
    }
    partial void OnDiffAdditionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffAdditionsText)); }
    partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffDeletionsText)); }
    partial void OnRoadblockCountChanged(int value) { OnPropertyChanged(nameof(HasRoadblock)); OnPropertyChanged(nameof(RoadblockTooltip)); }
    partial void OnDropHintAboveChanged(bool value) => OnPropertyChanged(nameof(IsDropTarget));
    partial void OnDropHintBelowChanged(bool value) => OnPropertyChanged(nameof(IsDropTarget));
    partial void OnCreationPhaseChanged(string? value)
    {
        OnPropertyChanged(nameof(HasCreationPhase));
        OnPropertyChanged(nameof(CreationPhaseLabel));
    }

    public void RefreshLocalized()
    {
        OnPropertyChanged(nameof(StatusLabel));
        OnPropertyChanged(nameof(PlanningBadge));
        OnPropertyChanged(nameof(CreatedAtFormatted));
        OnPropertyChanged(nameof(StepsText));
    }

    public static TaskRowViewModel FromEntity(TaskEntity t)
    {
        var row = new TaskRowViewModel { Id = t.Id, CreatedAt = t.CreatedAt };
        row.UpdateFromEntity(t);
        return row;
    }

    public void UpdateFromEntity(TaskEntity t)
    {
        // Any entity-backed refresh reflects a settled state, so the transient creation-phase
        // banner (set from the OperationProgress broadcast) has nothing left to announce.
        CreationPhase     = null;
        var (add, del) = ParseDiffStat(t.Worktree?.DiffStat);
        Number            = t.Number;
        Title             = t.Title;
        ListName          = t.List?.Name ?? "";
        Done              = t.Status == TaskStatus.Done;
        IsStarred         = t.IsStarred;
        IsMyDay           = t.IsMyDay;
        IsManual          = t.IsManual;
        Status            = t.Status;
        PlanningPhase     = t.PlanningPhase;
        Branch            = t.Worktree?.BranchName;
        DiffStat          = t.Worktree?.DiffStat;
        WorktreeState     = t.Worktree?.State;
        WorktreePath      = t.Worktree?.Path;
        ScheduledFor      = t.ScheduledFor;
        DiffAdditions     = add;
        DiffDeletions     = del;
        ParentTaskId      = t.ParentTaskId;
        CreatedBy         = t.CreatedBy;
        BlockedByTaskId   = t.BlockedByTaskId;
        DependsOnTaskId   = t.DependsOnTaskId;
        RoadblockCount    = t.RoadblockCount;
        FailureReason     = t.FailureReason;
        FailureTurnsUsed  = t.FailureTurnsUsed;
        FailureMaxTurns   = t.FailureMaxTurns;
    }

    // Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions".
    private static (int add, int del) ParseDiffStat(string? s)
    {
        if (string.IsNullOrWhiteSpace(s)) return (0, 0);
        int add = 0, del = 0;
        var parts = s.Split(new[] { ' ', ',', '\t' }, StringSplitOptions.RemoveEmptyEntries);
        foreach (var p in parts)
        {
            if (p.Length > 1 && p[0] == '+' && int.TryParse(p.AsSpan(1), out var a)) add = a;
            else if (p.Length > 1 && (p[0] == '-' || p[0] == '\u2212') && int.TryParse(p.AsSpan(1), out var d)) del = d;
        }
        return (add, del);
    }
}
