using Nexus.Api.Data;
using Nexus.Api.DTOs;
using Nexus.Api.Models;
using Nexus.Api.Repositories;

namespace Nexus.Api.Services;

public sealed class TaskService(
    ITaskRepository taskRepo,
    IActivityRepository activityRepo,
    INotificationService notificationService,
    IAgentService agentService,
    IHttpContextAccessor httpContextAccessor,
    ILiveUpdateService liveUpdateService,
    IStaleTaskRecoveryService staleTaskRecoveryService) : ITaskService
{
    public async Task<IReadOnlyList<WorkTask>> GetAllAsync(CancellationToken ct = default)
        => await taskRepo.GetAllAsync(ct);

    public async Task<WorkTask?> GetByIdAsync(Guid id, CancellationToken ct = default)
        => await taskRepo.GetByIdAsync(id, ct);

    public async Task<DashboardTaskDto?> GetDashboardTaskByIdAsync(Guid id, CancellationToken ct = default)
    {
        var allTasks = (await taskRepo.GetAllAsync(ct)).ToList();
        var task = allTasks.FirstOrDefault(t => t.Id == id);
        if (task is null) return null;

        var activity = await activityRepo.GetRecentForTasksAsync(allTasks.Select(t => t.Id), ct);
        return MapToDtoWithChildren(task, allTasks, activity);
    }

    public async Task<IReadOnlyList<WorkTask>> GetPendingApprovalAsync(CancellationToken ct = default)
        => await taskRepo.GetPendingApprovalAsync(ct);

    public async Task<WorkTask> CreateAsync(CreateTaskRequest request, CancellationToken ct = default)
    {
        var task = new WorkTask
        {
            Title = request.Title.Trim(),
            Priority = string.IsNullOrWhiteSpace(request.Priority) ? "Normal" : request.Priority.Trim(),
            ProjectId = request.ProjectId
        };
        await taskRepo.AddAsync(task, ct);
        await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task {task.Title} created", TaskId = task.Id }, ct);
        PublishBoardInvalidation(task.Id);
        return task;
    }

    public async Task<TaskOperationResult> ApproveAsync(Guid id, CancellationToken ct = default)
    {
        var task = await taskRepo.GetByIdAsync(id, ct);
        if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound);

        if (!TaskStateHelper.IsInProgressOrBlocked(task.State))
            return new TaskOperationResult(TaskOperationOutcome.InvalidState, task);

        task.State = TaskStateHelper.ToStateString(TaskState.Done);
        await taskRepo.UpdateAsync(task, ct);
        await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task {task.Title} approved", TaskId = task.Id }, ct);
        PublishBoardInvalidation(task.Id);
        return new TaskOperationResult(TaskOperationOutcome.Success, task);
    }

    public async Task<TaskOperationResult> RejectAsync(Guid id, CancellationToken ct = default)
    {
        var task = await taskRepo.GetByIdAsync(id, ct);
        if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound);

        if (!TaskStateHelper.IsInProgressOrBlocked(task.State))
            return new TaskOperationResult(TaskOperationOutcome.InvalidState, task);

        task.State = TaskStateHelper.ToStateString(TaskState.Backlog);
        await taskRepo.UpdateAsync(task, ct);
        await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task {task.Title} rejected, returned to backlog", TaskId = task.Id }, ct);
        PublishBoardInvalidation(task.Id);
        return new TaskOperationResult(TaskOperationOutcome.Success, task);
    }

    public async Task<TaskOperationResult> UpdateStateAsync(Guid id, string state, CancellationToken ct = default)
    {
        var canonical = TaskStateHelper.AllStates.FirstOrDefault(s => s.Equals(state, StringComparison.OrdinalIgnoreCase));
        if (canonical is null) return new TaskOperationResult(TaskOperationOutcome.InvalidState);

        var task = await taskRepo.GetByIdAsync(id, ct);
        if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound);

        var caller = ResolveCaller();
        if (!TaskStateHelper.CanChangeState(caller, task))
            return new TaskOperationResult(TaskOperationOutcome.InvalidState);

        return await UpdateTaskStatusInternalAsync(task, canonical, caller, "task", $"Task {task.Title} moved to {canonical}", ct);
    }

    public async Task<TaskOperationResult> UpdateAsync(Guid id, UpdateTaskRequest request, CancellationToken ct = default)
    {
        var task = await taskRepo.GetByIdAsync(id, ct);
        if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound);

        var changes = new List<string>();

        if (!string.IsNullOrWhiteSpace(request.Title) && !string.Equals(task.Title, request.Title.Trim(), StringComparison.Ordinal))
        {
            changes.Add($"Titel: \"{task.Title}\" → \"{request.Title.Trim()}\"");
            task.Title = request.Title.Trim();
        }
        if (!string.IsNullOrWhiteSpace(request.Priority) && !string.Equals(task.Priority, request.Priority.Trim(), StringComparison.OrdinalIgnoreCase))
        {
            changes.Add($"Priorität: {task.Priority} → {request.Priority.Trim()}");
            task.Priority = request.Priority.Trim();
        }
        if (request.ProjectId.HasValue)
        {
            changes.Add("Projekt-ID geändert");
            task.ProjectId = request.ProjectId.Value == Guid.Empty ? null : request.ProjectId;
        }

        await taskRepo.UpdateAsync(task, ct);
        var changeSummary = changes.Count > 0 ? string.Join("; ", changes) : "keine sichtbaren Änderungen";
        await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task \"{task.Title}\" aktualisiert: {changeSummary}", TaskId = task.Id }, ct);
        PublishBoardInvalidation(task.Id);
        return new TaskOperationResult(TaskOperationOutcome.Success, task);
    }

    public async Task<TaskOperationResult> DeleteAsync(Guid id, CancellationToken ct = default)
    {
        var task = await taskRepo.GetByIdAsync(id, ct);
        if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound);

        if (!TaskStateHelper.IsDoneOrBacklog(task.State))
            return new TaskOperationResult(TaskOperationOutcome.InvalidState, task);

        await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task {task.Title} deleted", TaskId = task.Id }, ct);
        await taskRepo.DeleteAsync(task, ct);
        PublishBoardInvalidation(task.Id);
        return new TaskOperationResult(TaskOperationOutcome.Success, task);
    }

    public async Task<IReadOnlyList<WorkTask>> GetOpenAsync(CancellationToken ct = default)
    {
        var all = await taskRepo.GetAllAsync(ct);
        return all.Where(t => !string.Equals(t.State, "Done", StringComparison.OrdinalIgnoreCase))
                  .OrderByDescending(t => t.CreatedAt)
                  .ToList();
    }

    public async Task<IReadOnlyList<WorkTask>> GetWaitingTasksAsync(CancellationToken ct = default)
    {
        var all = await taskRepo.GetAllAsync(ct);
        return all
            .Where(t => t.IsAgentTask && !string.Equals(t.State, "Done", StringComparison.OrdinalIgnoreCase))
            .OrderBy(t => t.ExpectedFrom != null ? 0 : 1)
            .ThenByDescending(t => t.UpdatedAt)
            .ToList();
    }

    public async Task<AgentWorkflowOverview> GetAgentWorkflowOverviewAsync(TimeSpan staleThreshold, CancellationToken ct = default)
    {
        var all = (await taskRepo.GetAllAsync(ct)).ToList();
        var threshold = DateTimeOffset.UtcNow - staleThreshold;
        var agentTasks = all.Where(t => t.IsAgentTask).ToList();
        var activity = await activityRepo.GetRecentForTasksAsync(agentTasks.Select(t => t.Id), ct);

        List<DashboardTaskDto> map(IEnumerable<WorkTask> tasks)
            => tasks.Select(task => MapToDtoWithChildren(task, all, activity)).ToList();

        var waitingForBao = map(agentTasks
            .Where(t => string.Equals(t.ExpectedFrom, "bao", StringComparison.OrdinalIgnoreCase) &&
                        !string.Equals(t.State, "Done", StringComparison.OrdinalIgnoreCase)));

        var waitingForIris = map(agentTasks
            .Where(t => string.Equals(t.ExpectedFrom, "iris", StringComparison.OrdinalIgnoreCase) &&
                        !string.Equals(t.State, "Done", StringComparison.OrdinalIgnoreCase)));

        var waitingForOthers = map(agentTasks
            .Where(t =>
            {
                var expected = (t.ExpectedFrom ?? "").ToLowerInvariant();
                return expected != "bao" && expected != "iris" && !string.IsNullOrWhiteSpace(expected) &&
                       !string.Equals(t.State, "Done", StringComparison.OrdinalIgnoreCase);
            }));

        var staleTasks = map(agentTasks
            .Where(t => string.Equals(t.State, "In progress", StringComparison.OrdinalIgnoreCase) && t.UpdatedAt < threshold));

        return new AgentWorkflowOverview(waitingForBao, waitingForIris, waitingForOthers, staleTasks, staleThreshold);
    }

    public async Task<WorkTask> CreateDashboardTaskAsync(
        string title, string? detail, string? source, string? priority,
        string? assignedTo, Guid? parentTaskId = null, CancellationToken ct = default)
    {
        if (parentTaskId.HasValue)
        {
            var parent = await taskRepo.GetByIdAsync(parentTaskId.Value, ct);
            if (parent is null)
                throw new ArgumentException($"Parent task {parentTaskId} not found.", nameof(parentTaskId));
        }

        var normalizedSource = string.IsNullOrWhiteSpace(source) ? "bao" : source.Trim().ToLowerInvariant();
        var normalizedAssignee = await NormalizeActorAsync(assignedTo, ct);
        var isVisibleDelegation = parentTaskId.HasValue;

        var task = new WorkTask
        {
            Title = title.Trim(),
            Detail = detail?.Trim(),
            Source = normalizedSource,
            Priority = string.IsNullOrWhiteSpace(priority) ? "Normal" : priority.Trim(),
            AssignedTo = normalizedAssignee,
            ParentTaskId = parentTaskId,
            IsAgentTask = isVisibleDelegation
        };
        await taskRepo.AddAsync(task, ct);

        var activityMessages = new List<string> { $"Task \"{task.Title}\" created ({task.Source})" };
        if (parentTaskId.HasValue)
        {
            activityMessages.Add($"Sichtbare Delegation erstellt: Child-Task von {parentTaskId.Value}.");
            await activityRepo.AddAsync(new ActivityEvent
            {
                Type = "delegation",
                Message = $"Board-first Delegation: Child-Task \"{task.Title}\" für {normalizedAssignee ?? task.Source} sichtbar angelegt.",
                TaskId = parentTaskId.Value
            }, ct);
        }

        await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = string.Join(" ", activityMessages), TaskId = task.Id }, ct);

        if (string.Equals(normalizedAssignee, "bao", StringComparison.OrdinalIgnoreCase))
        {
            await notificationService.CreateAsync(
                "task_assigned",
                $"Neue Aufgabe: {task.Title}",
                detail,
                "bao",
                task.Id,
                ct);
        }

        PublishBoardInvalidation(task.Id);
        return task;
    }

    public async Task<WorkTask> CreateAgentTaskAsync(
        string title, string? detail, string? source, string? priority,
        string? assignedTo, string? expectedFrom, Guid? parentTaskId = null, bool startsInProgress = true, string? initialState = null, CancellationToken ct = default)
    {
        var normalizedExpectedFrom = await NormalizeActorAsync(expectedFrom, ct);
        var task = await CreateDashboardTaskAsync(title, detail, source, priority, assignedTo, parentTaskId, ct);

        task.IsAgentTask = true;
        task.ExpectedFrom = normalizedExpectedFrom;
        task.State = ResolveInitialAgentTaskState(startsInProgress, initialState);
        await taskRepo.UpdateAsync(task, ct);

        await activityRepo.AddAsync(new ActivityEvent
        {
            Type = "agent_task",
            Message = $"Agent-Task created: \"{task.Title}\" (Source: {task.Source}, Expected: {task.ExpectedFrom ?? "none"})",
            TaskId = task.Id
        }, ct);

        if (parentTaskId.HasValue)
        {
            await activityRepo.AddAsync(new ActivityEvent
            {
                Type = "delegation",
                Message = $"Parent-/Child-Delegation sichtbar: Parent {parentTaskId.Value}, Child {task.Id}, wartet auf {task.ExpectedFrom ?? task.AssignedTo ?? "unbekannt"}.",
                TaskId = parentTaskId.Value
            }, ct);
        }

        await notificationService.CreateAsync(
            "agent_task_created",
            $"Neuer Agent-Task: {task.Title}",
            detail,
            "iris",
            task.Id,
            ct);

        PublishBoardInvalidation(task.Id);
        return task;
    }

    public async Task<TaskOperationResult> UpdateDashboardTaskAsync(
        Guid id, string? title, string? detail, string? source,
        string? priority, string? assignedTo, DateTimeOffset? dueDate = null, CancellationToken ct = default)
    {
        var caller = ResolveCaller();
        var task = await taskRepo.GetByIdAsync(id, ct);
        if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound);

        var changes = new List<string>();

        if (!string.IsNullOrWhiteSpace(title) && !string.Equals(task.Title, title.Trim(), StringComparison.Ordinal))
        {
            changes.Add($"Titel: \"{task.Title}\" → \"{title.Trim()}\"");
            task.Title = title.Trim();
        }
        if (detail is not null)
        {
            var newDetail = string.IsNullOrWhiteSpace(detail) ? null : detail.Trim();
            if (!string.Equals(task.Detail ?? "", newDetail ?? "", StringComparison.Ordinal))
            {
                changes.Add("Beschreibung aktualisiert");
                task.Detail = newDetail;
            }
        }
        if (!string.IsNullOrWhiteSpace(source))
            task.Source = source.Trim();
        if (!string.IsNullOrWhiteSpace(priority) && !string.Equals(task.Priority, priority.Trim(), StringComparison.OrdinalIgnoreCase))
        {
            changes.Add($"Priorität: {task.Priority} → {priority.Trim()}");
            task.Priority = priority.Trim();
        }
        if (assignedTo is not null)
        {
            var validated = await NormalizeActorAsync(assignedTo, ct);
            if (!string.Equals(task.AssignedTo ?? "", validated ?? "", StringComparison.OrdinalIgnoreCase))
            {
                changes.Add($"Zuständig: {task.AssignedTo ?? "niemand"} → {validated ?? "niemand"}");
                task.AssignedTo = validated;
            }
        }
        if (dueDate.HasValue && task.DueDate?.Date != dueDate.Value.Date)
        {
            changes.Add($"Fällig: {task.DueDate?.ToString("yyyy-MM-dd") ?? "kein Datum"} → {dueDate.Value:yyyy-MM-dd}");
            task.DueDate = dueDate;
        }

        await taskRepo.UpdateAsync(task, ct);

        var changeSummary = changes.Count > 0 ? string.Join("; ", changes) : "keine sichtbaren Änderungen";
        await activityRepo.AddAsync(new ActivityEvent
        {
            Type = "task",
            Message = $"Task \"{task.Title}\" aktualisiert von {caller}: {changeSummary}",
            TaskId = task.Id
        }, ct);

        if (changes.Count > 0 && caller == "bao")
        {
            await notificationService.CreateAsync(
                "task_content_changed",
                $"Bao hat \"{task.Title}\" geändert",
                changeSummary,
                "iris",
                task.Id,
                ct);
        }

        PublishBoardInvalidation(task.Id);
        return new TaskOperationResult(TaskOperationOutcome.Success, task);
    }

    public async Task<TaskOperationResult> UpdateStatusAsync(Guid id, string status, CancellationToken ct = default)
    {
        if (!TaskStateHelper.IsValidState(status))
            return new TaskOperationResult(TaskOperationOutcome.InvalidState);

        var task = await taskRepo.GetByIdAsync(id, ct);
        if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound);

        var caller = ResolveCaller();
        if (!TaskStateHelper.CanChangeState(caller, task))
            return new TaskOperationResult(TaskOperationOutcome.InvalidState);

        var canonical = TaskStateHelper.AllStates.First(s => s.Equals(status, StringComparison.OrdinalIgnoreCase));
        return await UpdateTaskStatusInternalAsync(task, canonical, caller, "task", null, ct);
    }

    public async Task<TaskOperationResult> StartCoordinationAsync(Guid id, CancellationToken ct = default)
    {
        var task = await taskRepo.GetByIdAsync(id, ct);
        if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound);

        if (!string.Equals(task.State, "Backlog", StringComparison.OrdinalIgnoreCase))
            return new TaskOperationResult(TaskOperationOutcome.Success, task);

        return await UpdateTaskStatusInternalAsync(
            task,
            canonical: TaskStateHelper.ToStateString(TaskState.InProgress),
            actor: "nexus-system",
            activityType: "delegation",
            activityMessage: $"Task \"{task.Title}\" → In progress (coordination started by child-task creation)",
            ct: ct);
    }

    public async Task<TaskOperationResult> CompleteViaQueueAsync(Guid id, CancellationToken ct = default)
    {
        var task = await taskRepo.GetByIdAsync(id, ct);
        if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound);

        task.State = "Done";
        await taskRepo.UpdateAsync(task, ct);
        await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task \"{task.Title}\" completed via queue", TaskId = task.Id }, ct);
        PublishBoardInvalidation(task.Id);
        return new TaskOperationResult(TaskOperationOutcome.Success, task);
    }

    public async Task<TaskOperationResult> CyclePriorityAsync(Guid id, CancellationToken ct = default)
    {
        var task = await taskRepo.GetByIdAsync(id, ct);
        if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound);

        task.Priority = task.Priority.ToLowerInvariant() switch
        {
            "high" => "Medium",
            "medium" => "Low",
            "low" => "High",
            _ => "Medium"
        };

        await taskRepo.UpdateAsync(task, ct);
        await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = $"Task \"{task.Title}\" priority → {task.Priority}", TaskId = task.Id }, ct);
        PublishBoardInvalidation(task.Id);
        return new TaskOperationResult(TaskOperationOutcome.Success, task);
    }

    public async Task<BoardResponse> GetBoardAsync(CancellationToken ct = default)
    {
        var all = (await taskRepo.GetAllAsync(ct)).ToList();
        var activity = await activityRepo.GetRecentForTasksAsync(all.Select(t => t.Id), ct);

        var offen = new List<DashboardTaskDto>();
        var inProgress = new List<DashboardTaskDto>();
        var review = new List<DashboardTaskDto>();
        var blocked = new List<DashboardTaskDto>();
        var done = new List<DashboardTaskDto>();

        foreach (var task in all)
        {
            var dto = MapToDtoWithChildren(task, all, activity);
            switch (task.State.ToLowerInvariant())
            {
                case "backlog": offen.Add(dto); break;
                case "in progress": inProgress.Add(dto); break;
                case "review": review.Add(dto); break;
                case "blocked": blocked.Add(dto); break;
                case "done": done.Add(dto); break;
                default: offen.Add(dto); break;
            }
        }

        offen.Sort(SortByPriorityThenCreatedAt);
        inProgress.Sort(SortByPriorityThenCreatedAt);
        review.Sort(SortByPriorityThenCreatedAt);
        blocked.Sort(SortByPriorityThenCreatedAt);
        done.Sort(SortByPriorityThenCreatedAt);

        return new BoardResponse(offen, inProgress, review, blocked, done);
    }

    public async Task<TaskBoardPageDto> GetBoardPageAsync(
        int doneLimit = 50,
        string? doneCursor = null,
        CancellationToken ct = default)
    {
        if (doneLimit is < 1 or > 100)
            throw new ArgumentOutOfRangeException(nameof(doneLimit), "Done limit must be between 1 and 100.");

        DateTimeOffset? doneBeforeUpdatedAt = null;
        Guid? doneBeforeId = null;
        if (doneCursor is not null)
        {
            if (!TaskBoardCursorCodec.TryDecode(doneCursor, out var cursor))
                throw new InvalidTaskBoardCursorException();

            doneBeforeUpdatedAt = cursor.UpdatedAt;
            doneBeforeId = cursor.Id;
        }

        var page = await taskRepo.GetBoardPageAsync(
            doneLimit,
            doneBeforeUpdatedAt,
            doneBeforeId,
            ct);

        var offen = new List<TaskBoardCardDto>();
        var inProgress = new List<TaskBoardCardDto>();
        var review = new List<TaskBoardCardDto>();
        var blocked = new List<TaskBoardCardDto>();

        foreach (var task in page.ActiveTasks)
        {
            switch (task.State.ToLowerInvariant())
            {
                case "in progress":
                    inProgress.Add(task);
                    break;
                case "review":
                    review.Add(task);
                    break;
                case "blocked":
                    blocked.Add(task);
                    break;
                default:
                    offen.Add(task);
                    break;
            }
        }

        var revision = page.Revision is null
            ? TaskBoardCursorCodec.Encode(DateTimeOffset.UnixEpoch, Guid.Empty)
            : TaskBoardCursorCodec.Encode(page.Revision.UpdatedAt, page.Revision.Id);

        var nextDoneCursor = page.HasMoreDone && page.DoneTasks.Count > 0
            ? TaskBoardCursorCodec.Encode(page.DoneTasks[^1].UpdatedAt, page.DoneTasks[^1].Id)
            : null;

        return new TaskBoardPageDto(
            revision,
            offen,
            inProgress,
            review,
            blocked,
            page.DoneTasks,
            nextDoneCursor,
            page.HasMoreDone);
    }

    public Task<TaskBoardCardDto?> GetBoardCardAsync(
        Guid id,
        CancellationToken ct = default)
        => taskRepo.GetBoardCardAsync(id, ct);

    private void PublishBoardInvalidation(Guid taskId)
        // The legacy dashboard SSE adapter replaces this content-minimized
        // signal with a compatibility snapshot for its own subscribers.
        // Mutations therefore never pay the old all-task/all-activity query.
        => liveUpdateService.Publish(
            "tasks.board.snapshot",
            new { taskId },
            "board");

    private static int SortByPriorityThenCreatedAt(DashboardTaskDto a, DashboardTaskDto b)
    {
        var priorityCompare = PriorityScore(b.Priority).CompareTo(PriorityScore(a.Priority));
        return priorityCompare != 0 ? priorityCompare : a.CreatedAt.CompareTo(b.CreatedAt);
    }

    private static int PriorityScore(string priority) => priority.ToLowerInvariant() switch
    {
        "high" => 3,
        "medium" => 2,
        "normal" => 2,
        "low" => 1,
        _ => 2
    };

    public async Task<TaskOperationResult> MoveTaskAsync(Guid id, string newState, CancellationToken ct = default)
    {
        var canonical = TaskStateHelper.AllStates.FirstOrDefault(s => s.Equals(newState, StringComparison.OrdinalIgnoreCase))
                        ?? TaskStateHelper.BoardGroupToState(newState);
        if (canonical is null)
            return new TaskOperationResult(TaskOperationOutcome.InvalidState);

        var task = await taskRepo.GetByIdAsync(id, ct);
        if (task is null) return new TaskOperationResult(TaskOperationOutcome.NotFound);

        var caller = ResolveCaller();
        if (!TaskStateHelper.CanChangeState(caller, task))
            return new TaskOperationResult(TaskOperationOutcome.InvalidState);

        return await UpdateTaskStatusInternalAsync(task, canonical, caller, "task", $"Task \"{task.Title}\" moved to {canonical}", ct);
    }

    public Task<int> ResetStaleAsync(int staleHours, CancellationToken ct = default)
    {
        var normalizedHours = Math.Max(1, staleHours);
        return ResetStaleInProgressTasksAsync(TimeSpan.FromHours(normalizedHours), ct);
    }

    public Task<int> ResetStaleInProgressTasksAsync(TimeSpan staleThreshold, CancellationToken ct = default)
        => staleTaskRecoveryService.ResetStaleInProgressTasksAsync(staleThreshold, ct);

    public async Task<IReadOnlyList<WorkTask>> GetChildTasksAsync(Guid parentId, CancellationToken ct = default)
    {
        var all = await taskRepo.GetAllAsync(ct);
        return all.Where(t => t.ParentTaskId == parentId)
                  .OrderByDescending(t => t.CreatedAt)
                  .ToList();
    }

    public async Task<List<ActivityEvent>> GetTaskActivityAsync(Guid taskId, CancellationToken ct = default)
    {
        var all = await activityRepo.GetRecentAsync(100, ct);
        return all.Where(e => e.TaskId == taskId).ToList();
    }

    private DashboardTaskDto MapToDtoWithChildren(WorkTask task, IReadOnlyList<WorkTask> allTasks, IEnumerable<ActivityEvent> activity)
    {
        var childTasks = allTasks.Where(t => t.ParentTaskId == task.Id)
            .OrderByDescending(t => t.UpdatedAt)
            .ToList();

        var childDtos = childTasks.Select(child => MapToDtoWithActivity(child, activity, allTasks)).ToList();
        var openChildTaskCount = childTasks.Count(child => !string.Equals(child.State, "Done", StringComparison.OrdinalIgnoreCase));

        var dto = MapToDtoWithActivity(task, activity, allTasks);
        return dto with
        {
            ChildTasks = childDtos,
            ChildTaskCount = childDtos.Count,
            OpenChildTaskCount = openChildTaskCount,
            HasVisibleDelegation = dto.ParentTaskId.HasValue || childDtos.Count > 0 || dto.IsAgentTask
        };
    }

    private static DashboardTaskDto MapToDto(WorkTask t) => new(
        t.Id, t.Title, t.Detail, t.Source, t.State, t.Priority, t.AssignedTo,
        t.ParentTaskId, t.DueDate, t.CreatedAt, t.UpdatedAt,
        t.IsAgentTask, t.ExpectedFrom,
        ProjectId: t.ProjectId);

    private static DashboardTaskDto MapToDtoWithActivity(WorkTask t, IEnumerable<ActivityEvent> activity, IReadOnlyList<WorkTask>? _allTasks = null)
    {
        var last = activity
            .Where(e => e.TaskId == t.Id)
            .OrderByDescending(e => e.CreatedAt)
            .FirstOrDefault();

        return new DashboardTaskDto(
            t.Id, t.Title, t.Detail, t.Source, t.State, t.Priority, t.AssignedTo,
            t.ParentTaskId, t.DueDate, t.CreatedAt, t.UpdatedAt,
            t.IsAgentTask, t.ExpectedFrom,
            last?.Message,
            last?.CreatedAt,
            null,
            0,
            0,
            t.ParentTaskId.HasValue || t.IsAgentTask,
            t.ProjectId);
    }

    private async Task<string?> NormalizeActorAsync(string? actorId, CancellationToken ct)
    {
        var allowedActors = AgentIdentityCatalog.BuildAllowedActorIds(await agentService.GetAllowedAgentIdsAsync(ct));
        return AgentIdentityCatalog.NormalizeActorId(actorId, allowedActors);
    }

    private static string ResolveInitialAgentTaskState(bool startsInProgress, string? initialState)
    {
        if (!string.IsNullOrWhiteSpace(initialState))
        {
            var canonical = TaskStateHelper.AllStates.FirstOrDefault(state =>
                state.Equals(initialState, StringComparison.OrdinalIgnoreCase));
            if (canonical is not null)
                return canonical;
        }

        return startsInProgress
            ? TaskStateHelper.ToStateString(TaskState.InProgress)
            : TaskStateHelper.ToStateString(TaskState.Backlog);
    }

    private string ResolveCaller()
    {
        var httpContext = httpContextAccessor.HttpContext;
        if (httpContext is null) return "nexus-system";

        var user = httpContext.User;
        if (user?.Identity?.IsAuthenticated != true)
            return "";

        var canUseAgentHint = user.IsInRole("Service") ||
                              user.IsInRole("owner") ||
                              user.IsInRole("admin");
        if (canUseAgentHint)
        {
            var agentHeader = httpContext.Request.Headers["X-Agent-Id"].FirstOrDefault();
            if (!string.IsNullOrWhiteSpace(agentHeader))
                return agentHeader.Trim().ToLowerInvariant();
        }

        if (user.IsInRole("owner") || user.IsInRole("admin"))
            return "bao";

        var nameClaim = user?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
        return nameClaim?.ToLowerInvariant() ?? "";
    }

    private async Task<TaskOperationResult> UpdateTaskStatusInternalAsync(
        WorkTask task,
        string canonical,
        string actor,
        string activityType,
        string? activityMessage,
        CancellationToken ct)
    {
        task.State = canonical;
        await taskRepo.UpdateAsync(task, ct);
        await activityRepo.AddAsync(new ActivityEvent
        {
            Type = activityType,
            Message = activityMessage ?? $"Task \"{task.Title}\" → {canonical}",
            TaskId = task.Id
        }, ct);
        await CreateStatusChangeNotificationsAsync(task, canonical, actor, ct);
        PublishBoardInvalidation(task.Id);
        return new TaskOperationResult(TaskOperationOutcome.Success, task);
    }

    private async Task CreateStatusChangeNotificationsAsync(WorkTask task, string canonical, string caller, CancellationToken ct)
    {
        if (string.Equals(canonical, "Review", StringComparison.OrdinalIgnoreCase))
        {
            await notificationService.CreateAsync(
                "task_review",
                $"Task zur Überprüfung: {task.Title}",
                $"Status auf Review geändert von {caller}",
                "bao",
                task.Id,
                ct);
        }
        else if (string.Equals(canonical, "Blocked", StringComparison.OrdinalIgnoreCase))
        {
            await notificationService.CreateAsync(
                "task_blocked",
                $"Aufgabe blockiert: {task.Title}",
                $"Die Task wurde von {caller} auf Blockiert gesetzt.",
                "iris",
                task.Id,
                ct);
        }
        else
        {
            if (caller == "bao")
            {
                await notificationService.CreateAsync(
                    "task_status_changed",
                    $"Bao hat Status geändert: {task.Title}",
                    $"Status → {canonical}",
                    "iris",
                    task.Id,
                    ct);
            }
            else if (caller == "iris")
            {
                await notificationService.CreateAsync(
                    "task_status_changed",
                    $"Iris hat Status geändert: {task.Title}",
                    $"Status → {canonical}",
                    "bao",
                    task.Id,
                    ct);
            }
        }
    }
}
