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 WorkerClient _worker;
    private readonly IServiceProvider _services;

    // Current task row (set by IslandsShellViewModel via Bind)
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(RunNowCommand))]
    private TaskRowViewModel? _task;

    // Editable fields
    [ObservableProperty] private string _editableTitle = "";
    [ObservableProperty] private string _notes = "";
    [ObservableProperty] private string _promptInput = "";

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

    // Agent strip fields
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(RunNowCommand))]
    private string _agentStatusLabel = "Idle";
    public bool IsRunning => AgentStatusLabel == "Running";
    public bool IsDone    => AgentStatusLabel == "Done";
    public bool IsFailed  => AgentStatusLabel == "Failed";

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
    [NotifyCanExecuteChangedFor(nameof(ResetCommand))]
    private bool _showFailedActions;

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

    partial void OnAgentStatusLabelChanged(string value)
    {
        OnPropertyChanged(nameof(IsRunning));
        OnPropertyChanged(nameof(IsDone));
        OnPropertyChanged(nameof(IsFailed));
        OnPropertyChanged(nameof(IsAgentSectionEnabled));
        ShowFailedActions = value == "Failed";
    }
    [ObservableProperty] private string? _model;

    // Agent settings overrides
    [ObservableProperty] private string _taskModelSelection = "(inherit)";
    [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()
    {
        "(inherit)", "sonnet", "opus", "haiku",
    };

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

    private bool _suppressAgentSave;
    private CancellationTokenSource? _agentSaveCts;

    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 = "";

    // 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 OpenWorktreeCommand can show the modal as a dialog
    public Func<WorktreeModalViewModel, System.Threading.Tasks.Task>? ShowWorktreeModal { get; set; }

    // Set by the view so ApproveMergeCommand can show the modal as a dialog
    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; }

    public DetailsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, WorkerClient 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))
            {
                RunNowCommand.NotifyCanExecuteChanged();
                ContinueCommand.NotifyCanExecuteChanged();
                ResetCommand.NotifyCanExecuteChanged();
                ApproveMergeCommand.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);
        };
    }

    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();

    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 == "(inherit)" ? 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("(inherit)", "", ""));
            var agents = await _worker.GetAgentsAsync();
            foreach (var a in agents) TaskAgentOptions.Add(a);

            TaskModelSelection = string.IsNullOrWhiteSpace(entity.Model) ? "(inherit)" : 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();
        _claudeBuf.Clear();

        if (row == null)
        {
            _subscribedTaskId = null;
            EditableTitle = "";
            Notes = "";
            Model = null;
            WorktreePath = null;
            WorktreeStateLabel = null;
            BranchLine = null;
            AgentStatusLabel = "Idle";
            LatestRunSessionId = null;
            ShowFailedActions = false;
            _suppressAgentSave = true;
            try
            {
                TaskModelSelection = "(inherit)";
                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;
            Notes = entity.Notes ?? "";
            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;

            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 });
        }
        catch (OperationCanceledException) { }
    }

    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();
        ApproveMergeCommand.NotifyCanExecuteChanged();
    }

    partial void OnWorktreeStateLabelChanged(string? value)
    {
        ApproveMergeCommand.NotifyCanExecuteChanged();
    }

    [RelayCommand]
    private async System.Threading.Tasks.Task SendPromptAsync()
    {
        if (string.IsNullOrWhiteSpace(PromptInput) || Task == null) return;
        Log.Add(new LogLineViewModel { Kind = LogKind.Msg, Text = $"[you] {PromptInput}" });
        // TODO: WorkerClient has no SendPromptAsync — no matching hub method found.
        // When the worker gains a "SendPrompt" hub method, call:
        //   await _worker.SendPromptAsync(Task.Id, PromptInput);
        PromptInput = "";
        await System.Threading.Tasks.Task.CompletedTask;
    }

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

    [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;
        }
        await using var ctx = _dbFactory.CreateDbContext();
        var repo = new TaskRepository(ctx);
        await repo.DeleteAsync(row.Id);
        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 SaveNotesAsync()
    {
        if (Task == null) return;
        await using var ctx = _dbFactory.CreateDbContext();
        var repo = new TaskRepository(ctx);
        var entity = await repo.GetByIdAsync(Task.Id);
        if (entity == null) return;
        entity.Notes = Notes;
        await repo.UpdateAsync(entity);
    }

    [RelayCommand(CanExecute = nameof(CanMerge))]
    private async System.Threading.Tasks.Task ApproveMergeAsync()
    {
        if (Task == null || ShowMergeModal == null) return;
        var vm = _services.GetRequiredService<MergeModalViewModel>();
        await vm.InitializeAsync(Task.Id, Task.Title);
        await ShowMergeModal(vm);
    }

    private bool CanMerge() =>
        Task != null && _worker.IsConnected && WorktreePath != null && WorktreeStateLabel == "Active";

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

    [RelayCommand(CanExecute = nameof(CanRunNow))]
    private async System.Threading.Tasks.Task RunNowAsync()
    {
        if (Task == null) return;
        AgentStatusLabel = "Running";
        try
        {
            await _worker.RunNowAsync(Task.Id);
        }
        catch
        {
            AgentStatusLabel = "Failed";
            throw;
        }
    }

    private bool CanRunNow() =>
        Task != null && _worker.IsConnected && !IsRunning;

    [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 && ShowFailedActions && !string.IsNullOrEmpty(LatestRunSessionId);

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

        var branchName = $"claudedo/{Task.Id.Replace("-", "")}";
        var ok = await ConfirmAsync($"Discard worktree and reset task?\nThis deletes branch {branchName} and all uncommitted changes.");
        if (!ok) return;

        await _worker.ResetTaskAsync(Task.Id);
    }

    private bool CanReset() =>
        Task != null && _worker.IsConnected && ShowFailedActions;
}

public sealed partial class SubtaskRowViewModel : ViewModelBase
{
    public required string Id { get; init; }
    [ObservableProperty] private string _title = "";
    [ObservableProperty] private bool _done;
}
