using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Nexus.Api.Data;
using Nexus.Api.Models;
using Nexus.Api.Repositories;
using Nexus.Api.Services;

namespace Nexus.Api.Controllers;

[Authorize]
[ApiController]
[Route("api/dashboard")]
public class DashboardController(
    IDashboardService dashboardService,
    ITaskService taskService,
    IActivityRepository activityService,
    IHttpContextAccessor httpContextAccessor,
    IAgentService agentService,
    IConfiguration configuration,
    INotificationService notificationService,
    ILiveUpdateService liveUpdateService) : ControllerBase
{
    [HttpGet("status")]
    public async Task<DashboardStatus> GetStatus()
        => await dashboardService.GetStatusAsync();

    [HttpGet("agents")]
    public async Task<List<DashboardAgentInfo>> GetAgents()
        => await dashboardService.GetAgentsAsync();

    [HttpGet("operations")]
    public async Task<List<FeedEntry>> GetOperations(
        [FromQuery] int limit = 20,
        [FromQuery] string? agent = null)
        => await dashboardService.GetOperationsAsync(limit, agent);

    [HttpPost("chat/send")]
    [Authorize(Roles = "owner")]
    [EnableRateLimiting("agents")]
    [Obsolete("Legacy adapter. Use POST /api/v1/chat.")]
    public async Task<ChatResponse> SendChat([FromBody] ChatRequest request)
    {
        if (string.IsNullOrWhiteSpace(request.Message))
            return new ChatResponse(false, null, "Message is required");

        var agentId = string.IsNullOrWhiteSpace(request.AgentId) ? "iris" : request.AgentId.Trim();
        return await dashboardService.SendChatAsync(agentId, request.Message.Trim());
    }

    [HttpGet("chat/messages")]
    public async Task<List<MessageEntry>> GetMessages(
        [FromQuery] string? sessionKey,
        [FromQuery] int limit = 50,
        [FromQuery] int offset = 0)
        => await dashboardService.GetMessagesAsync(sessionKey, limit, offset);

    [HttpGet("queue")]
    public async Task<List<QueueItem>> GetQueue(CancellationToken ct)
        => await dashboardService.GetQueueAsync(ct);

    [HttpGet("gateway")]
    public async Task<GatewayRuntimeInfo> GetGateway(CancellationToken ct)
        => await dashboardService.GetGatewayInfoAsync(ct);

    [HttpDelete("queue/{id}")]
    [Authorize(Roles = "owner")]
    [EnableRateLimiting("agents")]
    public async Task<ActionResult> DeleteQueueItem(string id, [FromQuery] string? source, CancellationToken ct)
    {
        var result = await dashboardService.DeleteQueueItemAsync(id, source, ct);
        return result.Outcome switch
        {
            QueueDeleteOutcome.Deleted => NoContent(),
            QueueDeleteOutcome.NotFound => NotFound(new { error = "Queue item not found" }),
            QueueDeleteOutcome.GatewayError => StatusCode(502, new { error = "Gateway could not delete cron job" }),
            QueueDeleteOutcome.TaskNotFound => NotFound(new { error = "Task not found" }),
            QueueDeleteOutcome.InvalidTaskId => BadRequest(new { error = "Invalid task id" }),
            QueueDeleteOutcome.Ignored when string.Equals(source, "cron", StringComparison.OrdinalIgnoreCase)
                => StatusCode(
                    StatusCodes.Status410Gone,
                    new
                    {
                        error = "Legacy cron deletion has been removed",
                        recovery = $"Use DELETE /api/v1/openclaw/cron/{Uri.EscapeDataString(id)} with Idempotency-Key and a current resource hash."
                    }),
            _ => StatusCode(500, new { error = "Internal error" })
        };
    }

    [HttpPut("queue/{id}/priority")]
    public async Task<ActionResult> ChangeQueuePriority(string id, CancellationToken ct)
    {
        var result = await dashboardService.CycleQueuePriorityAsync(id, ct);
        return result.Outcome switch
        {
            QueuePriorityOutcome.Ignored => Ok(new { status = "ignored", reason = "Cron job priorities are managed by the gateway" }),
            QueuePriorityOutcome.TaskNotFound => NotFound(new { error = "Task not found" }),
            QueuePriorityOutcome.InvalidTaskId => BadRequest(new { error = "Invalid task id" }),
            _ => Ok(new { status = "ok", priority = result.NewPriority })
        };
    }

    [HttpGet("agents/{id}/model")]
    public async Task<ActionResult<AgentModelInfo>> GetAgentModel(string id)
    {
        var info = await dashboardService.GetAgentModelAsync(id);
        return info is null
            ? NotFound(new { error = $"Agent '{id}' not found or gateway unreachable" })
            : Ok(info);
    }

    [HttpPut("agents/{id}/model")]
    public async Task<ActionResult> SetAgentModel(string id, [FromBody] SetModelRequest request)
    {
        if (string.IsNullOrWhiteSpace(request.Model))
            return BadRequest(new { error = "Model is required" });

        var ok = await dashboardService.SetAgentModelAsync(id, request.Model);
        return ok ? Ok(new { status = "ok", model = request.Model }) : StatusCode(502, new { error = "Gateway did not accept the change" });
    }

    [HttpGet("agents/{id}/activity")]
    public async Task<List<AgentActivityEntry>> GetAgentActivity(string id, [FromQuery] int limit = 5)
        => await dashboardService.GetAgentActivityAsync(id, limit);

    [HttpGet("models")]
    public async Task<ActionResult<List<ModelOption>>> GetAvailableModels(CancellationToken ct)
        => Ok(await dashboardService.GetAvailableModelsAsync(ct));

    // ── Task Endpoints ──

    [HttpGet("tasks")]
    public async Task<List<DashboardTaskDto>> GetTasks(CancellationToken ct)
    {
        var tasks = await taskService.GetOpenAsync(ct);
        return tasks.Select(task => MapToDto(task)).ToList();
    }

    [HttpPost("tasks")]
    public async Task<ActionResult<DashboardTaskDto>> CreateTask(
        [FromBody] CreateDashboardTaskRequest request, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(request.Title))
            return BadRequest(new { error = "Title is required." });

        try
        {
            var task = await taskService.CreateDashboardTaskAsync(
                request.Title, request.Detail, request.Source, request.Priority, request.AssignedTo, request.ParentTaskId, ct);
            return Created(
                $"/api/dashboard/tasks/{task.Id}",
                MapToDto(task, TaskOperation(task, "created")));
        }
        catch (ArgumentException ex)
        {
            return BadRequest(new { error = ex.Message });
        }
    }

    [HttpPut("tasks/{id:guid}")]
    public async Task<ActionResult<DashboardTaskDto>> UpdateTask(
        Guid id, [FromBody] UpdateDashboardTaskRequest request, CancellationToken ct)
    {
        var result = await taskService.UpdateDashboardTaskAsync(
            id, request.Title, request.Detail, request.Source, request.Priority, request.AssignedTo, request.DueDate, ct);
        return result.Outcome switch
        {
            TaskOperationOutcome.NotFound => NotFound(new { error = "Task not found." }),
            _ => Ok(MapToDto(
                result.Task!,
                TaskOperation(result.Task!, "updated")))
        };
    }

    [HttpDelete("tasks/{id:guid}")]
    public async Task<ActionResult> DeleteTask(Guid id, CancellationToken ct)
    {
        var result = await taskService.DeleteAsync(id, ct);
        return result.Outcome switch
        {
            TaskOperationOutcome.NotFound => NotFound(new { error = "Task not found." }),
            TaskOperationOutcome.InvalidState => StatusCode(403, new { error = "Only tasks in 'Done' or 'Backlog' state can be deleted." }),
            _ => Ok(MapToDto(
                result.Task!,
                TaskOperation(result.Task!, "deleted")))
        };
    }

    [HttpPatch("tasks/{id:guid}/status")]
    public async Task<ActionResult<DashboardTaskDto>> UpdateTaskStatus(
        Guid id, [FromBody] UpdateDashboardTaskStatusRequest request, CancellationToken ct)
    {
        // Enforce workflow rules based on caller agent
        var currentTask = await taskService.GetByIdAsync(id, ct);
        if (currentTask is null)
            return NotFound(new { error = "Task not found." });

        // Resolve caller agent from header or JWT
        var callerAgent = await ResolveCallerAgentAsync(ct);

        // Nur Iris und Bao dürfen Status ändern
        if (!TaskStateHelper.CanChangeState(callerAgent, currentTask))
        {
            return StatusCode(403, new { error = "Statusänderungen sind nur Iris und Bao vorbehalten. Sub-Agenten können Tasks nicht verschieben." });
        }

        var result = await taskService.UpdateStatusAsync(id, request.Status, ct);
        return result.Outcome switch
        {
            TaskOperationOutcome.InvalidState => BadRequest(new { error = $"Unsupported status: '{request.Status}'. Valid: {string.Join(", ", TaskStateHelper.AllStates)}" }),
            TaskOperationOutcome.NotFound => NotFound(new { error = "Task not found." }),
            _ => Ok(MapToDto(
                result.Task!,
                TaskOperation(result.Task!, "updated")))
        };
    }

    // ── Task Board Endpoints ──

    [HttpGet("tasks/board")]
    public async Task<ActionResult<BoardResponse>> GetBoard(CancellationToken ct)
    {
        if (!await CanReadBoardAsync(ct))
            return Unauthorized();

        return Ok(await taskService.GetBoardAsync(ct));
    }

    [HttpGet("live")]
    public async Task Live(
        [FromQuery] string forUser = "bao",
        [FromQuery] int notificationLimit = 50,
        [FromQuery] long? afterSequence = null,
        CancellationToken ct = default)
    {
        Response.Headers.Append("Content-Type", "text/event-stream");
        Response.Headers.Append("Cache-Control", "no-cache, no-store, must-revalidate");
        Response.Headers.Append("Connection", "keep-alive");
        Response.Headers.Append("X-Accel-Buffering", "no");

        async Task WriteEventAsync(string eventName, object payload)
        {
            await Response.WriteAsync($"event: {eventName}\n", ct);
            await Response.WriteAsync($"data: {System.Text.Json.JsonSerializer.Serialize(payload)}\n\n", ct);
            await Response.Body.FlushAsync(ct);
        }

        var currentSequence = liveUpdateService.CurrentSequence;
        // Subscribe before loading the snapshot so updates published while the
        // snapshot is assembled are queued and delivered afterwards.
        var subscription = await liveUpdateService.SubscribeAsync(currentSequence, ct);
        var initial = new DashboardLiveSnapshotDto(
            await taskService.GetBoardAsync(ct),
            await notificationService.GetSnapshotAsync(forUser, notificationLimit, ct: ct),
            new LiveCursorDto(currentSequence, DateTimeOffset.UtcNow, "live"));
        await WriteEventAsync("snapshot", initial);

        // A fresh snapshot is the authoritative baseline for this legacy
        // adapter. Only advance this cursor after an update has actually been
        // written; CurrentSequence may include events still queued below.
        var lastSent = currentSequence;

        while (!ct.IsCancellationRequested)
        {
            using var iteration = CancellationTokenSource.CreateLinkedTokenSource(ct);
            var readTask = subscription.Reader.WaitToReadAsync(iteration.Token).AsTask();
            var heartbeatTask = Task.Delay(TimeSpan.FromSeconds(20), iteration.Token);
            var completed = await Task.WhenAny(readTask, heartbeatTask);

            if (completed == heartbeatTask)
            {
                iteration.Cancel();
                await WriteEventAsync(
                    "heartbeat",
                    new LiveCursorDto(lastSent, DateTimeOffset.UtcNow, "live"));
                continue;
            }

            iteration.Cancel();
            if (!await readTask)
                break;

            while (subscription.Reader.TryRead(out var envelope))
            {
                if (envelope.Sequence <= lastSent)
                    continue;

                if (envelope.Type == "notifications.snapshot")
                {
                    var snapshot = envelope.Payload as NotificationSnapshotDto
                        ?? await notificationService.GetSnapshotAsync(forUser, notificationLimit, ct: ct);
                    if (!string.Equals(snapshot.ForUser, forUser, StringComparison.OrdinalIgnoreCase))
                        continue;
                    envelope = envelope with { Payload = snapshot };
                }

                if (envelope.Type == "tasks.board.snapshot")
                {
                    envelope = envelope with { Payload = await taskService.GetBoardAsync(ct) };
                }

                await WriteEventAsync("update", new DashboardLiveEventDto(
                    envelope,
                    new LiveCursorDto(envelope.Sequence, envelope.Timestamp, "live")));
                lastSent = envelope.Sequence;
            }
        }
    }

    [HttpPatch("tasks/{id:guid}/move")]
    public async Task<ActionResult<DashboardTaskDto>> MoveTask(
        Guid id, [FromBody] MoveTaskRequest request, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(request.State))
            return BadRequest(new { error = "State is required." });

        // Enforce workflow rules based on caller agent
        var currentTask = await taskService.GetByIdAsync(id, ct);
        if (currentTask is null)
            return NotFound(new { error = "Task not found." });

        // Resolve caller agent from header or JWT
        var callerAgent = await ResolveCallerAgentAsync(ct);

        // Nur Iris und Bao dürfen Status ändern
        if (!TaskStateHelper.CanChangeState(callerAgent, currentTask))
        {
            return StatusCode(403, new { error = "Statusänderungen sind nur Iris und Bao vorbehalten. Sub-Agenten können Tasks nicht verschieben." });
        }

        var result = await taskService.MoveTaskAsync(id, request.State, ct);
        return result.Outcome switch
        {
            TaskOperationOutcome.InvalidState => BadRequest(new { error = $"Unsupported state: '{request.State}'. Valid: {string.Join(", ", TaskStateHelper.AllStates)}" }),
            TaskOperationOutcome.NotFound => NotFound(new { error = "Task not found." }),
            _ => Ok(MapToDto(
                result.Task!,
                TaskOperation(result.Task!, "updated")))
        };
    }

    /// <summary>
    /// Resolves the caller identity from a verified principal. X-Agent-Id is
    /// treated only as an authenticated, allow-listed actor hint.
    /// </summary>
    private async Task<string> ResolveCallerAgentAsync(CancellationToken ct)
    {
        var httpContext = httpContextAccessor.HttpContext;
        if (httpContext is null) return "";

        var agentHeader = await RequestAuthorizationHelper.ResolveAllowedAgentHeaderAsync(
            httpContext,
            agentService,
            configuration,
            ct);
        if (!string.IsNullOrWhiteSpace(agentHeader))
            return agentHeader;

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

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

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

    // ── New Endpoints: Reset Stale, Children, Activity ──

    [HttpPost("tasks/reset-stale")]
    public async Task<ActionResult<ResetStaleResponse>> ResetStale(
        [FromBody] ResetStaleRequest request, CancellationToken ct)
    {
        var threshold = TimeSpan.FromHours(Math.Max(1, request.StaleHours));
        var count = await taskService.ResetStaleInProgressTasksAsync(threshold, ct);
        var operation = OperationResultFactory.FromHttpContext(
            HttpContext,
            count > 0 ? "completed" : "noop",
            new EntityRefDto("task-board", "active", "Task Board"));
        return Ok(new ResetStaleResponse(count, operation));
    }

    [HttpGet("tasks/{id:guid}/children")]
    public async Task<ActionResult<List<DashboardTaskDto>>> GetChildren(Guid id, CancellationToken ct)
    {
        var board = await taskService.GetBoardAsync(ct);
        var children = board.Offen
            .Concat(board.InProgress)
            .Concat(board.Review)
            .Concat(board.Blocked)
            .Concat(board.Done)
            .Where(task => task.ParentTaskId == id)
            .OrderByDescending(task => task.UpdatedAt)
            .ToList();

        return Ok(children);
    }

    [HttpGet("tasks/{id:guid}")]
    public async Task<ActionResult<DashboardTaskDto>> GetTask(Guid id, CancellationToken ct)
    {
        var task = await taskService.GetDashboardTaskByIdAsync(id, ct);
        if (task is null) return NotFound(new { error = "Task not found." });
        return Ok(task);
    }

    [HttpGet("tasks/{id:guid}/activity")]
    public async Task<ActionResult<List<ActivityEvent>>> GetTaskActivity(Guid id, CancellationToken ct)
    {
        var events = await taskService.GetTaskActivityAsync(id, ct);
        return Ok(events);
    }

    [HttpPost("tasks/{id:guid}/activity")]
    public async Task<ActionResult<ActivityItemDto>> PostTaskActivity(
        Guid id, [FromBody] PostActivityRequest request, CancellationToken ct)
    {
        var task = await taskService.GetByIdAsync(id, ct);
        if (task is null) return NotFound(new { error = "Task not found." });

        if (string.IsNullOrWhiteSpace(request.Message))
            return BadRequest(new { error = "Message is required." });

        var ev = new ActivityEvent
        {
            Type = request.Type ?? "comment",
            Message = request.Message.Trim(),
            TaskId = id
        };

        await activityService.AddAsync(ev, ct);
        var entity = new EntityRefDto("task", task.Id.ToString(), task.Title);
        var operation = OperationResultFactory.FromHttpContext(
            HttpContext,
            "created",
            new EntityRefDto("activity", ev.Id.ToString(), ev.Type),
            affectedRefs: [entity]);
        return Created(
            $"/api/dashboard/tasks/{id}/activity/{ev.Id}",
            new ActivityItemDto(
                ev.Id,
                ev.Type,
                ev.Message,
                ev.CreatedAt,
                entity,
                operation));
    }

    // ── Agent Workflow Endpoints (Iris Overview) ──

    /// <summary>
    /// Returns agent-tasks that are still open and waiting for input.
    /// Iris uses this to see who she is waiting for.
    /// </summary>
    [HttpGet("tasks/agent-waiting")]
    public async Task<ActionResult<List<DashboardTaskDto>>> GetAgentWaitingTasks(CancellationToken ct)
    {
        var waiting = await taskService.GetWaitingTasksAsync(ct);
        return Ok(waiting.Select(task => MapToDto(task)).ToList());
    }

    /// <summary>
    /// Returns a complete agent-workflow overview grouped by expected respondent
    /// + stale detection. This is the main Iris dashboard data.
    /// </summary>
    [HttpGet("tasks/agent-overview")]
    public async Task<ActionResult<AgentWorkflowOverview>> GetAgentOverview(
        CancellationToken ct, [FromQuery] int staleHours = 2)
    {
        var threshold = TimeSpan.FromHours(Math.Max(1, staleHours));
        return Ok(await taskService.GetAgentWorkflowOverviewAsync(threshold, ct));
    }

    /// <summary>
    /// Creates an agent-task: a task that is tracked as originating from the agent workflow.
    /// Sub-agents (programmer, reviewer) can only CREATE, not move state.
    /// </summary>
    [HttpPost("tasks/agent")]
    public async Task<ActionResult<DashboardTaskDto>> CreateAgentTask(
        [FromBody] CreateAgentTaskRequest request, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(request.Title))
            return BadRequest(new { error = "Title is required." });

        try
        {
            var task = await taskService.CreateAgentTaskAsync(
                request.Title, request.Detail, request.Source ?? "iris",
                request.Priority, request.AssignedTo, request.ExpectedFrom,
                request.ParentTaskId, request.StartsInProgress, request.InitialState, ct);

            return Created(
                $"/api/dashboard/tasks/{task.Id}",
                MapToDto(task, TaskOperation(task, "created")));
        }
        catch (ArgumentException ex)
        {
            return BadRequest(new { error = ex.Message });
        }
    }

    private OperationResultDto TaskOperation(WorkTask task, string status)
    {
        var affected = new List<EntityRefDto>();
        if (task.ProjectId is { } projectId)
            affected.Add(new EntityRefDto("project", projectId.ToString()));
        if (task.ParentTaskId is { } parentTaskId)
            affected.Add(new EntityRefDto("task", parentTaskId.ToString(), "Parent task"));

        return OperationResultFactory.FromHttpContext(
            HttpContext,
            status,
            new EntityRefDto("task", task.Id.ToString(), task.Title),
            affectedRefs: affected);
    }

    private static DashboardTaskDto MapToDto(
        WorkTask t,
        OperationResultDto? operation = null) => 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,
        Operation: operation);

    private Task<bool> CanReadBoardAsync(CancellationToken _)
        => Task.FromResult(RequestAuthorizationHelper.HasVerifiedAuthentication(HttpContext, configuration));
}
