using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Threading;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;

namespace ClaudeDo.Ui.Services;

public sealed record WorkerLogEntry(string Message, WorkerLogLevel Level, DateTime TimestampUtc);

sealed class IndefiniteRetryPolicy : IRetryPolicy
{
    private static readonly TimeSpan[] _delays =
    [
        TimeSpan.Zero,
        TimeSpan.FromSeconds(2),
        TimeSpan.FromSeconds(5),
        TimeSpan.FromSeconds(10),
        TimeSpan.FromSeconds(30),
    ];

    public TimeSpan? NextRetryDelay(RetryContext retryContext) =>
        _delays[Math.Min(retryContext.PreviousRetryCount, _delays.Length - 1)];
}

public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerClient
{
    private readonly HubConnection _hub;
    private CancellationTokenSource? _startCts;
    private Task _retryLoopTask = Task.CompletedTask;
    private readonly object _startLock = new();

    [ObservableProperty]
    private bool _isConnected;

    [ObservableProperty]
    private bool _isReconnecting;

    public ObservableCollection<ActiveTaskDto> ActiveTasks { get; } = new();

    public event Action<string, string, DateTime>? TaskStartedEvent;
    public event Action<string, string, string, DateTime>? TaskFinishedEvent;
    public event Action<string, string>? TaskMessageEvent;
    public event Action<string>? TaskUpdatedEvent;
    public event Action<string, string, string>? TaskQuestionAskedEvent;
    public event Action<string, string>? TaskQuestionResolvedEvent;
    public event Action<string, IReadOnlyList<string>, string>? HandoffRequestedEvent;
    public event Action? ConnectionRestoredEvent;
    public event Action<string>? WorktreeUpdatedEvent;
    public event Action<string>? ListUpdatedEvent;
    public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;

    public event Action? NotesUpdatedEvent;
    public event Action? PrepStartedEvent;
    public event Action<string>? PrepLineEvent;
    public event Action<bool>? PrepFinishedEvent;

    public event Action<string>? RefineStartedEvent;
    public event Action<string, bool, string?>? RefineFinishedEvent;

    public event Action<UsageSnapshotDto>? UsageUpdatedEvent;

    public event Action<string, string, int, int>? OperationProgressEvent;
    public event Action<string, string, int>? MergeProgressEvent;

    public event Action<string, string>? PlanningMergeStartedEvent;
    public event Action<string, string>? PlanningSubtaskMergedEvent;
    public event Action<string, string, IReadOnlyList<string>, bool>? PlanningMergeConflictEvent;
    public event Action<string, string?>? PlanningMergeAbortedEvent;
    public event Action<string>? PlanningCompletedEvent;

    public event Action<PrimeFiredEvent>? PrimeFired;

    public string? LastApproveTarget { get; private set; }

    public IReadOnlyList<ActiveTaskDto> GetActiveTasks() => ActiveTasks.ToList();

    public WorkerClient(string signalRUrl)
    {
        _hub = new HubConnectionBuilder()
            .WithUrl(signalRUrl)
            .WithAutomaticReconnect(new IndefiniteRetryPolicy())
            .AddJsonProtocol(options =>
            {
                options.PayloadSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
            })
            .Build();

        _hub.Reconnected += async _ =>
        {
            Dispatcher.UIThread.Post(() => { IsConnected = true; IsReconnecting = false; });
            await SeedActiveTasksAsync();
            Dispatcher.UIThread.Post(() => ConnectionRestoredEvent?.Invoke());
        };

        _hub.Reconnecting += _ =>
        {
            Dispatcher.UIThread.Post(() => { IsConnected = false; IsReconnecting = true; });
            return Task.CompletedTask;
        };

        _hub.Closed += _ =>
        {
            Dispatcher.UIThread.Post(() =>
            {
                IsConnected = false;
                IsReconnecting = false;
                ActiveTasks.Clear();
            });
            return Task.CompletedTask;
        };

        _hub.On<string, string, DateTime>("TaskStarted", (slot, taskId, startedAt) =>
        {
            Dispatcher.UIThread.Post(() =>
            {
                ActiveTasks.Add(new ActiveTaskDto(slot, taskId, startedAt));
                TaskStartedEvent?.Invoke(slot, taskId, startedAt);
            });
        });

        _hub.On<string, string, string, DateTime>("TaskFinished", (slot, taskId, status, finishedAt) =>
        {
            Dispatcher.UIThread.Post(() =>
            {
                var existing = ActiveTasks.FirstOrDefault(t => t.TaskId == taskId);
                if (existing is not null)
                    ActiveTasks.Remove(existing);
                TaskFinishedEvent?.Invoke(slot, taskId, status, finishedAt);
            });
        });

        _hub.On<string, string>("TaskMessage", (taskId, line) =>
        {
            Dispatcher.UIThread.Post(() => TaskMessageEvent?.Invoke(taskId, line));
        });

        _hub.On<string>("TaskUpdated", taskId =>
        {
            Dispatcher.UIThread.Post(() => TaskUpdatedEvent?.Invoke(taskId));
        });

        _hub.On<string, string, string>("TaskQuestionAsked", (taskId, questionId, question) =>
        {
            Dispatcher.UIThread.Post(() => TaskQuestionAskedEvent?.Invoke(taskId, questionId, question));
        });

        _hub.On<string, string>("TaskQuestionResolved", (taskId, questionId) =>
        {
            Dispatcher.UIThread.Post(() => TaskQuestionResolvedEvent?.Invoke(taskId, questionId));
        });

        _hub.On<string, IReadOnlyList<string>, string>("HandoffRequested", (taskId, survivingTaskIds, nextPhase) =>
        {
            Dispatcher.UIThread.Post(() => HandoffRequestedEvent?.Invoke(taskId, survivingTaskIds, nextPhase));
        });

        _hub.On<string>("WorktreeUpdated", taskId =>
        {
            Dispatcher.UIThread.Post(() => WorktreeUpdatedEvent?.Invoke(taskId));
        });

        _hub.On<string>("ListUpdated", listId =>
        {
            Dispatcher.UIThread.Post(() => ListUpdatedEvent?.Invoke(listId));
        });

        _hub.On<string, WorkerLogLevel, DateTime>("WorkerLog", (message, level, timestampUtc) =>
        {
            Dispatcher.UIThread.Post(() =>
                WorkerLogReceivedEvent?.Invoke(new WorkerLogEntry(message, level, timestampUtc)));
        });

        _hub.On<string, string, int, int>("OperationProgress", (opKey, phase, current, total) =>
        {
            Dispatcher.UIThread.Post(() =>
            {
                OperationProgressEvent?.Invoke(opKey, phase, current, total);
                MergeProgressEvent?.Invoke(opKey, phase, current);
            });
        });

        _hub.On<string, string>("PlanningMergeStarted", (planningTaskId, targetBranch) =>
        {
            Dispatcher.UIThread.Post(() => PlanningMergeStartedEvent?.Invoke(planningTaskId, targetBranch));
        });

        _hub.On<string, string>("PlanningSubtaskMerged", (planningTaskId, subtaskId) =>
        {
            Dispatcher.UIThread.Post(() => PlanningSubtaskMergedEvent?.Invoke(planningTaskId, subtaskId));
        });

        _hub.On<string, string, IReadOnlyList<string>, bool>("PlanningMergeConflict", (planningTaskId, subtaskId, conflictedFiles, externallyDriven) =>
        {
            Dispatcher.UIThread.Post(() => PlanningMergeConflictEvent?.Invoke(planningTaskId, subtaskId, conflictedFiles, externallyDriven));
        });

        _hub.On<string, string?>("PlanningMergeAborted", (planningTaskId, reason) =>
        {
            Dispatcher.UIThread.Post(() => PlanningMergeAbortedEvent?.Invoke(planningTaskId, reason));
        });

        _hub.On<string>("PlanningCompleted", planningTaskId =>
        {
            Dispatcher.UIThread.Post(() => PlanningCompletedEvent?.Invoke(planningTaskId));
        });

        _hub.On<Guid, bool, string, DateTimeOffset>("PrimeFired", (id, ok, msg, when) =>
        {
            Dispatcher.UIThread.Post(() => PrimeFired?.Invoke(new PrimeFiredEvent(id, ok, msg, when)));
        });

        _hub.On("NotesUpdated", () => Dispatcher.UIThread.Post(() => NotesUpdatedEvent?.Invoke()));
        _hub.On("PrepStarted", () => Dispatcher.UIThread.Post(() => PrepStartedEvent?.Invoke()));
        _hub.On<string>("PrepLine", line => Dispatcher.UIThread.Post(() => PrepLineEvent?.Invoke(line)));
        _hub.On<bool>("PrepFinished", ok => Dispatcher.UIThread.Post(() => PrepFinishedEvent?.Invoke(ok)));

        _hub.On<string>("RefineStarted", id =>
            Dispatcher.UIThread.Post(() => RefineStartedEvent?.Invoke(id)));
        _hub.On<string, bool, string?>("RefineFinished", (id, ok, err) =>
            Dispatcher.UIThread.Post(() => RefineFinishedEvent?.Invoke(id, ok, err)));

        _hub.On<UsageSnapshotDto>("UsageUpdated", snapshot =>
            Dispatcher.UIThread.Post(() => UsageUpdatedEvent?.Invoke(snapshot)));
    }

    public Task StartAsync()
    {
        lock (_startLock)
        {
            if (!_retryLoopTask.IsCompleted)
                return Task.CompletedTask;

            var old = _startCts;
            _startCts = new CancellationTokenSource();
            old?.Cancel();
            old?.Dispose();

            _retryLoopTask = ConnectWithRetryAsync(_startCts.Token);
        }
        return Task.CompletedTask;
    }

    private async Task ConnectWithRetryAsync(CancellationToken ct)
    {
        var delays = new[] { 0, 2, 5, 10, 30 };
        int attempt = 0;
        Dispatcher.UIThread.Post(() => IsReconnecting = true);
        while (!ct.IsCancellationRequested)
        {
            try
            {
                await _hub.StartAsync(ct);
                Dispatcher.UIThread.Post(() => { IsConnected = true; IsReconnecting = false; });
                await SeedActiveTasksAsync();
                Dispatcher.UIThread.Post(() => ConnectionRestoredEvent?.Invoke());
                return;
            }
            catch (OperationCanceledException)
            {
                return;
            }
            catch
            {
                var delay = delays[Math.Min(attempt++, delays.Length - 1)];
                try { await Task.Delay(TimeSpan.FromSeconds(delay), ct); }
                catch (OperationCanceledException) { return; }
            }
        }
    }

    public async Task StopAsync()
    {
        _startCts?.Cancel();
        try { await _retryLoopTask; } catch (OperationCanceledException) { } catch { /* swallow */ }
        try { await _hub.StopAsync(); } catch { /* swallow */ }
    }

    /// <summary>Invoke a hub method, returning default (null) when the worker is offline or errors.</summary>
    private async Task<T?> TryInvokeAsync<T>(string method, params object?[] args)
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        var ok = false;
        try
        {
            var result = await _hub.InvokeCoreAsync<T>(method, args);
            ok = true;
            return result;
        }
        catch { return default; }
        finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
    }

    /// <summary>Times a void hub invoke without changing its exception/cancellation behavior.</summary>
    private async Task InvokeTimedAsync(string method, Func<Task> invoke)
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        var ok = false;
        try
        {
            await invoke();
            ok = true;
        }
        finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
    }

    /// <summary>Times a hub invoke that returns a value, without changing its exception/cancellation behavior.</summary>
    private async Task<T> InvokeTimedAsync<T>(string method, Func<Task<T>> invoke)
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        var ok = false;
        try
        {
            var result = await invoke();
            ok = true;
            return result;
        }
        finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
    }

    public async Task RunNowAsync(string taskId)
    {
        await InvokeTimedAsync("RunNow", () => _hub.InvokeAsync("RunNow", taskId));
    }

    public async Task ContinueTaskAsync(string taskId, string followUpPrompt)
    {
        await InvokeTimedAsync("ContinueTask", () => _hub.InvokeAsync("ContinueTask", taskId, followUpPrompt));
    }

    public async Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer)
    {
        try { await InvokeTimedAsync<bool>("AnswerTaskQuestion", () => _hub.InvokeAsync<bool>("AnswerTaskQuestion", taskId, questionId, answer)); }
        catch { /* offline or already resolved — the UI clears optimistically */ }
    }

    public Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId)
        => TryInvokeAsync<PendingQuestionDto>("GetPendingQuestion", taskId);

    public async Task ResetTaskAsync(string taskId)
    {
        await InvokeTimedAsync("ResetTask", () => _hub.InvokeAsync("ResetTask", taskId));
    }

    public async Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage)
    {
        return await InvokeTimedAsync<MergeResultDto>("MergeTask", () => _hub.InvokeAsync<MergeResultDto>(
            "MergeTask", taskId, targetBranch, removeWorktree, commitMessage));
    }

    public Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch)
        => InvokeTimedAsync<MergeResultDto>("StartConflictMerge", () => _hub.InvokeAsync<MergeResultDto>("StartConflictMerge", taskId, targetBranch));

    public Task<MergeConflictDocumentsDto> GetMergeConflictDocumentsAsync(string taskId)
        => InvokeTimedAsync<MergeConflictDocumentsDto>("GetMergeConflictDocuments", () => _hub.InvokeAsync<MergeConflictDocumentsDto>("GetMergeConflictDocuments", taskId));

    public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent)
        => InvokeTimedAsync("WriteConflictResolution", () => _hub.InvokeAsync("WriteConflictResolution", taskId, path, resolvedContent));

    public Task<MergeResultDto> ContinueConflictMergeAsync(string taskId)
        => InvokeTimedAsync<MergeResultDto>("ContinueConflictMerge", () => _hub.InvokeAsync<MergeResultDto>("ContinueConflictMerge", taskId));

    public Task AbortConflictMergeAsync(string taskId)
        => InvokeTimedAsync("AbortConflictMerge", () => _hub.InvokeAsync("AbortConflictMerge", taskId));

    public Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId)
        => TryInvokeAsync<MergeTargetsDto>("GetMergeTargets", taskId);

    public async Task CancelTaskAsync(string taskId)
    {
        await InvokeTimedAsync("CancelTask", () => _hub.InvokeAsync("CancelTask", taskId));
    }

    public async Task<(bool Ok, string? Error)> DeleteTaskAsync(string taskId)
    {
        try
        {
            await InvokeTimedAsync("DeleteTask", () => _hub.InvokeAsync("DeleteTask", taskId, CancellationToken.None));
            return (true, null);
        }
        catch (HubException ex)
        {
            return (false, ex.Message);
        }
        catch (InvalidOperationException)
        {
            // Hub connection is not active (worker offline / not yet connected).
            return (false, "Worker is offline. Reconnect and try again.");
        }
    }

    public async Task WakeQueueAsync()
    {
        await InvokeTimedAsync("WakeQueue", () => _hub.InvokeAsync("WakeQueue"));
    }

    public async Task<List<AgentInfo>> GetAgentsAsync()
        => await TryInvokeAsync<List<AgentInfo>>("GetAgents") ?? [];

    public async Task RefreshAgentsAsync()
    {
        await InvokeTimedAsync("RefreshAgents", () => _hub.InvokeAsync("RefreshAgents"));
    }

    public async Task<SeedResultDto?> RestoreDefaultAgentsAsync()
        => await InvokeTimedAsync<SeedResultDto>("RestoreDefaultAgents", () => _hub.InvokeAsync<SeedResultDto>("RestoreDefaultAgents"));

    private async Task SeedActiveTasksAsync()
    {
        try
        {
            var active = await InvokeTimedAsync<List<ActiveTaskDto>>("GetActive", () => _hub.InvokeAsync<List<ActiveTaskDto>>("GetActive"));
            Dispatcher.UIThread.Post(() =>
            {
                ActiveTasks.Clear();
                foreach (var a in active)
                    ActiveTasks.Add(new ActiveTaskDto(a.Slot, a.TaskId, a.StartedAt));
            });
        }
        catch (HubException)
        {
            // Expected: worker doesn't support GetActive yet
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine($"SeedActiveTasksAsync failed: {ex}");
        }
    }

    public async ValueTask DisposeAsync()
    {
        _startCts?.Cancel();
        try { await _retryLoopTask; } catch (OperationCanceledException) { } catch { /* swallow */ }
        await _hub.DisposeAsync();
    }

    public Task<AppSettingsDto?> GetAppSettingsAsync()
        => TryInvokeAsync<AppSettingsDto>("GetAppSettings");

    public async Task UpdateAppSettingsAsync(AppSettingsDto dto)
    {
        await InvokeTimedAsync("UpdateAppSettings", () => _hub.InvokeAsync("UpdateAppSettings", dto));
    }

    public async Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync()
        => await TryInvokeAsync<List<PrimeScheduleDto>>("ListPrimeSchedules") ?? new List<PrimeScheduleDto>();

    public async Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto)
        => await InvokeTimedAsync<PrimeScheduleDto>("UpsertPrimeSchedule", () => _hub.InvokeAsync<PrimeScheduleDto>("UpsertPrimeSchedule", dto));

    public async Task DeletePrimeScheduleAsync(Guid id)
    {
        try { await InvokeTimedAsync("DeletePrimeSchedule", () => _hub.InvokeAsync("DeletePrimeSchedule", id)); }
        catch { /* offline */ }
    }

    private static string IsoDay(DateOnly d) => d.ToString("yyyy-MM-dd");

    public Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end)
        => TryInvokeAsync<string>("GetWeekReport", IsoDay(start), IsoDay(end));

    public Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end)
        => InvokeTimedAsync<string>("GenerateWeekReport", () => _hub.InvokeAsync<string>("GenerateWeekReport", IsoDay(start), IsoDay(end)));

    public Task<bool> RunDailyPrepNowAsync()
        => InvokeTimedAsync<bool>("RunDailyPrepNow", () => _hub.InvokeAsync<bool>("RunDailyPrepNow"));

    public Task RefineTaskAsync(string taskId) => InvokeTimedAsync("RefineTask", () => _hub.InvokeAsync("RefineTask", taskId));

    public Task ClearMyDayAsync()
        => InvokeTimedAsync("ClearMyDay", () => _hub.InvokeAsync("ClearMyDay"));

    public async Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day)
        => await TryInvokeAsync<List<DailyNoteDto>>("GetDailyNotes", IsoDay(day)) ?? new List<DailyNoteDto>();

    public async Task<List<DailyNoteDto>> GetDailyNotesBetweenAsync(DateOnly start, DateOnly end)
        => await TryInvokeAsync<List<DailyNoteDto>>("GetDailyNotesBetween", IsoDay(start), IsoDay(end)) ?? new List<DailyNoteDto>();

    public async Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text)
        => await InvokeTimedAsync<DailyNoteDto>("AddDailyNote", () => _hub.InvokeAsync<DailyNoteDto>("AddDailyNote", IsoDay(day), text));

    public async Task UpdateDailyNoteAsync(string id, string text)
        => await InvokeTimedAsync("UpdateDailyNote", () => _hub.InvokeAsync("UpdateDailyNote", id, text));

    public async Task DeleteDailyNoteAsync(string id)
        => await InvokeTimedAsync("DeleteDailyNote", () => _hub.InvokeAsync("DeleteDailyNote", id));

    public async Task<string> GetLastPrepLogAsync()
        => await TryInvokeAsync<string>("GetLastPrepLog") ?? string.Empty;

    public Task<WorkerBuildInfoDto?> GetWorkerBuildInfoAsync()
        => TryInvokeAsync<WorkerBuildInfoDto>("GetWorkerBuildInfo");

    public async Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync()
        => await TryInvokeAsync<List<WorkerLogEntry>>("GetRecentLogs") ?? new List<WorkerLogEntry>();

    public async Task UpdateListAsync(UpdateListDto dto)
    {
        await InvokeTimedAsync("UpdateList", () => _hub.InvokeAsync("UpdateList", dto));
    }

    public async Task UpdateListConfigAsync(UpdateListConfigDto dto)
    {
        await InvokeTimedAsync("UpdateListConfig", () => _hub.InvokeAsync("UpdateListConfig", dto));
    }

    public Task<ListConfigDto?> GetListConfigAsync(string listId)
        => TryInvokeAsync<ListConfigDto>("GetListConfig", listId);

    public async Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto)
    {
        await InvokeTimedAsync("UpdateTaskAgentSettings", () => _hub.InvokeAsync("UpdateTaskAgentSettings", dto));
    }

    public async Task<List<string>> GetRepoImportFoldersAsync()
        => await InvokeTimedAsync<List<string>>("GetRepoImportFolders", () => _hub.InvokeAsync<List<string>>("GetRepoImportFolders"));

    public Task SetRepoImportFoldersAsync(List<string> folders)
        => InvokeTimedAsync("SetRepoImportFolders", () => _hub.InvokeAsync("SetRepoImportFolders", folders));

    public async Task<List<SessionSkillDto>> GetSessionSkillsAsync()
        => await TryInvokeAsync<List<SessionSkillDto>>("GetSessionSkills") ?? [];

    public Task<List<string>> InstallSessionSkillAsync(string url)
        => InvokeTimedAsync<List<string>>("InstallSessionSkill", () => _hub.InvokeAsync<List<string>>("InstallSessionSkill", url));

    public Task UpdateSessionSkillAsync(string sourceUrl)
        => InvokeTimedAsync("UpdateSessionSkill", () => _hub.InvokeAsync("UpdateSessionSkill", sourceUrl));

    public Task RemoveSessionSkillAsync(string sourceUrl)
        => InvokeTimedAsync("RemoveSessionSkill", () => _hub.InvokeAsync("RemoveSessionSkill", sourceUrl));

    public async Task<BaseDirtyWarningDto?> SetTaskStatusAsync(string taskId, ClaudeDo.Data.Models.TaskStatus status)
    {
        var result = await InvokeTimedAsync<SetTaskStatusResultDto>("SetTaskStatus", () => _hub.InvokeAsync<SetTaskStatusResultDto>("SetTaskStatus", taskId, status.ToString()));
        return result?.BaseDirty;
    }

    public Task SetTaskDoneAsync(string taskId)
        => InvokeTimedAsync("SetTaskDone", () => _hub.InvokeAsync("SetTaskDone", taskId));

    public Task UnsetTaskDoneAsync(string taskId)
        => InvokeTimedAsync("UnsetTaskDone", () => _hub.InvokeAsync("UnsetTaskDone", taskId));

    public Task DequeueTaskAsync(string taskId)
        => InvokeTimedAsync("DequeueTask", () => _hub.InvokeAsync("DequeueTask", taskId));

    public async Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch)
    {
        LastApproveTarget = targetBranch;
        return await InvokeTimedAsync<MergeResultDto>("ApproveReview", () => _hub.InvokeAsync<MergeResultDto>("ApproveReview", taskId, targetBranch));
    }

    public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch)
        => TryInvokeAsync<MergePreviewDto>("PreviewMerge", taskId, targetBranch);

    public async Task RejectReviewToQueueAsync(string taskId, string feedback)
    {
        await InvokeTimedAsync("RejectReviewToQueue", () => _hub.InvokeAsync("RejectReviewToQueue", taskId, feedback));
    }

    public async Task RejectReviewToIdleAsync(string taskId)
    {
        await InvokeTimedAsync("RejectReviewToIdle", () => _hub.InvokeAsync("RejectReviewToIdle", taskId));
    }

    public async Task CancelReviewAsync(string taskId)
    {
        await InvokeTimedAsync("CancelReview", () => _hub.InvokeAsync("CancelReview", taskId));
    }

    public async Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null)
        => await InvokeTimedAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", () => _hub.InvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", listId));

    public async Task<WorktreeResetDto?> ResetAllWorktreesAsync()
        => await InvokeTimedAsync<WorktreeResetDto>("ResetAllWorktrees", () => _hub.InvokeAsync<WorktreeResetDto>("ResetAllWorktrees"));

    public async Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId)
        => await TryInvokeAsync<List<WorktreeOverviewDto>>("GetWorktreesOverview", listId)
           ?? new List<WorktreeOverviewDto>();

    public async Task<(bool Ok, string? Error)> SetWorktreeStateAsync(string taskId, WorktreeState newState)
    {
        try
        {
            var ok = await InvokeTimedAsync<bool>("SetWorktreeState", () => _hub.InvokeAsync<bool>("SetWorktreeState", taskId, newState));
            return (ok, null);
        }
        catch (HubException ex)
        {
            return (false, ex.Message);
        }
        catch (Exception)
        {
            return (false, "Worker offline.");
        }
    }

    public async Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId)
        => await InvokeTimedAsync<ForceRemoveResultDto>("ForceRemoveWorktree", () => _hub.InvokeAsync<ForceRemoveResultDto>("ForceRemoveWorktree", taskId));

    public async Task<PlanningSessionStartInfo> StartPlanningSessionAsync(string taskId, CancellationToken ct = default)
        => await InvokeTimedAsync<PlanningSessionStartInfo>("StartPlanningSessionAsync", () => _hub.InvokeAsync<PlanningSessionStartInfo>("StartPlanningSessionAsync", taskId, ct));

    public async Task<PlanningSessionResumeInfo> ResumePlanningSessionAsync(string taskId, CancellationToken ct = default)
        => await InvokeTimedAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", () => _hub.InvokeAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", taskId, ct));

    public async Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default)
        => await InvokeTimedAsync("SubmitTaskForReview", () => _hub.InvokeAsync("SubmitTaskForReview", taskId, ct));

    public async Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
        => await InvokeTimedAsync<LaunchSpec>("GetInteractiveLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetInteractiveLaunchSpec", taskId, ct));

    public async Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
        => await InvokeTimedAsync<LaunchSpec>("GetAdHocLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetAdHocLaunchSpec", directory, ct));

    public async Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct = default)
        => await InvokeTimedAsync<LaunchSpec>("GetMergeHelperLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetMergeHelperLaunchSpec", taskIds, listId, handlerTaskId, ct));

    public async Task<string> CreateMergeHelperTaskAsync(
        IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
        => await InvokeTimedAsync<string>("CreateMergeHelperTask", () => _hub.InvokeAsync<string>("CreateMergeHelperTask", taskIds, listId, title, descriptionHeader, ct));

    public async Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
        string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase, CancellationToken ct = default)
        => await InvokeTimedAsync<LaunchSpec>("GetMergeHelperHandoffLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetMergeHelperHandoffLaunchSpec", taskId, survivingTaskIds, nextPhase, ct));

    public async Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
        => await InvokeTimedAsync<LaunchSpec>("GetPlanningStartLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetPlanningStartLaunchSpec", taskId, ct));

    public async Task<LaunchSpec> GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default)
        => await InvokeTimedAsync<LaunchSpec>("GetPlanningResumeLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetPlanningResumeLaunchSpec", taskId, ct));

    public async Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
        => await InvokeTimedAsync<DiscardPlanningOutcome>("DiscardPlanningSessionAsync", () => _hub.InvokeAsync<DiscardPlanningOutcome>("DiscardPlanningSessionAsync", taskId, dequeueQueuedChildren, ct));

    public async Task<int> FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default)
        => await InvokeTimedAsync<int>("FinalizePlanningSessionAsync", () => _hub.InvokeAsync<int>("FinalizePlanningSessionAsync", taskId, queueAgentTasks, ct));

    public async Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default)
        => await InvokeTimedAsync<int>("GetPendingDraftCountAsync", () => _hub.InvokeAsync<int>("GetPendingDraftCountAsync", taskId, ct));

    public async Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId)
        => await TryInvokeAsync<List<SubtaskDiffDto>>("GetPlanningAggregate", planningTaskId) ?? [];

    public async Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch)
        => await InvokeTimedAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", () => _hub.InvokeAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", planningTaskId, targetBranch));

    public async Task ContinuePlanningMergeAsync(string planningTaskId)
    {
        await InvokeTimedAsync("ContinuePlanningMerge", () => _hub.InvokeAsync("ContinuePlanningMerge", planningTaskId));
    }

    public async Task AbortPlanningMergeAsync(string planningTaskId)
    {
        await InvokeTimedAsync("AbortPlanningMerge", () => _hub.InvokeAsync("AbortPlanningMerge", planningTaskId));
    }

    public async Task<IReadOnlyList<PlanningMergeConflictStateDto>> GetActiveExternalPlanningMergeConflictsAsync()
        => await TryInvokeAsync<List<PlanningMergeConflictStateDto>>("GetActiveExternalPlanningMergeConflicts")
           ?? [];

    public async Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default)
    {
        await InvokeTimedAsync("QueuePlanningSubtasksAsync", () => _hub.InvokeAsync("QueuePlanningSubtasksAsync", parentTaskId, ct));
    }

    public Task<ClaudeBinDto?> GetClaudeBinAsync()
        => TryInvokeAsync<ClaudeBinDto>("GetClaudeBin");

    public Task<ClaudeBinDto?> SetClaudeBinAsync(string? value)
        => TryInvokeAsync<ClaudeBinDto>("SetClaudeBin", value);

    public Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync()
        => TryInvokeAsync<OnlineInboxStateDto>("GetOnlineInboxState");

    public async Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input)
        => await InvokeTimedAsync("SetOnlineInboxConfig", () => _hub.InvokeAsync("SetOnlineInboxConfig", input));

    public async Task SetOnlineInboxAuthAsync(string refreshToken)
        => await InvokeTimedAsync("SetOnlineInboxAuth", () => _hub.InvokeAsync("SetOnlineInboxAuth", refreshToken));

    public async Task ClearOnlineInboxAuthAsync()
        => await InvokeTimedAsync("ClearOnlineInboxAuth", () => _hub.InvokeAsync("ClearOnlineInboxAuth"));

    public Task<UsageSnapshotDto?> GetUsageSnapshotAsync()
        => TryInvokeAsync<UsageSnapshotDto>("GetUsageSnapshot");

    public Task<UsageSnapshotDto?> RefreshUsageAsync()
        => TryInvokeAsync<UsageSnapshotDto>("RefreshUsage");

    public async Task<IReadOnlyList<ModelUsageRowDto>> GetModelUsageAsync(DateOnly from, DateOnly to)
        => await TryInvokeAsync<List<ModelUsageRowDto>>("GetModelUsage", from, to) ?? [];

    public async Task<IReadOnlyList<TaskUsageRowDto>> GetTaskUsageAsync(DateOnly from, DateOnly to)
        => await TryInvokeAsync<List<TaskUsageRowDto>>("GetTaskUsage", from, to) ?? [];

    public Task<TokenTrackerStatusDto?> GetTokenTrackerStatusAsync()
        => TryInvokeAsync<TokenTrackerStatusDto>("GetTokenTrackerStatus");

    public Task<TokenTrackerStatusDto?> RefreshTokenTrackerAsync()
        => TryInvokeAsync<TokenTrackerStatusDto>("RefreshTokenTracker");

    public Task<TokenTrackerStatusDto?> InstallTokenTrackerAsync()
        => TryInvokeAsync<TokenTrackerStatusDto>("InstallTokenTracker");

    public Task<TokenTrackerDashboardDto?> OpenTokenTrackerDashboardAsync()
        => TryInvokeAsync<TokenTrackerDashboardDto>("OpenTokenTrackerDashboard");

    public Task<TicketSettingsDto?> GetTicketSettingsAsync()
        => TryInvokeAsync<TicketSettingsDto>("GetTicketSettings");

    public Task SetTicketApiBaseUrlAsync(string? baseUrl)
        => InvokeTimedAsync("SetTicketApiBaseUrl", () => _hub.InvokeAsync("SetTicketApiBaseUrl", baseUrl));

    public Task SetTicketTokenAsync(string token)
        => InvokeTimedAsync("SetTicketToken", () => _hub.InvokeAsync("SetTicketToken", token));

    public Task ClearTicketTokenAsync()
        => InvokeTimedAsync("ClearTicketToken", () => _hub.InvokeAsync("ClearTicketToken"));

    public Task<TicketConnectionDto?> TestTicketConnectionAsync()
        => TryInvokeAsync<TicketConnectionDto>("TestTicketConnection");

    public async Task<List<TicketProjectDto>> GetTicketProjectsAsync()
        => await TryInvokeAsync<List<TicketProjectDto>>("GetTicketProjects") ?? [];

    public Task<TicketImportResultDto> ImportTicketsAsync(string listId)
        => InvokeTimedAsync<TicketImportResultDto>("ImportTickets", () => _hub.InvokeAsync<TicketImportResultDto>("ImportTickets", listId));

    // IWorkerClient explicit implementations (drop typed return values)
    async Task IWorkerClient.StartPlanningSessionAsync(string taskId, CancellationToken ct)
        => await StartPlanningSessionAsync(taskId, ct);
    async Task IWorkerClient.ResumePlanningSessionAsync(string taskId, CancellationToken ct)
        => await ResumePlanningSessionAsync(taskId, ct);
    async Task<DiscardPlanningOutcome> IWorkerClient.DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren, CancellationToken ct)
        => await DiscardPlanningSessionAsync(taskId, dequeueQueuedChildren, ct);
    async Task IWorkerClient.FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks, CancellationToken ct)
        => await FinalizePlanningSessionAsync(taskId, queueAgentTasks, ct);
    async Task<int> IWorkerClient.GetPendingDraftCountAsync(string taskId, CancellationToken ct)
        => await GetPendingDraftCountAsync(taskId, ct);
}

