using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using System.Diagnostics;
using System.Security.Claims;
using Nexus.Api.DTOs;
using Nexus.Api.Integrations;
using Nexus.Api.Http;
using Nexus.Api.Repositories;
using Nexus.Api.Services;

namespace Nexus.Api.Controllers;

[ApiController]
[Route("api/v1/agents")]
public class AgentsController(
    IAgentService agentService,
    IOpenClawChatService chat,
    IActivityRepository activityRepo,
    IOpenClawAgentConfigurationService agentConfiguration,
    IDashboardService dashboardService,
    ILogger<AgentsController> logger) : ControllerBase
{
    [HttpGet]
    [ProducesResponseType(typeof(IReadOnlyList<AgentListResponse>), StatusCodes.Status200OK)]
    public async Task<IResult> GetAgents(CancellationToken ct)
    {
        var agents = await agentService.GetAgentsAsync(ct);
        return Results.Ok(agents.Select(a => new AgentListResponse(
            a.Id, a.Name, a.Role, a.Model, a.Status.ToString(), a.LastSeen, a.Workspace, a.Description)));
    }

    [HttpGet("{id}")]
    [ProducesResponseType(typeof(AgentDetailResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    public async Task<IResult> GetAgent(string id, CancellationToken ct)
    {
        var agent = await agentService.GetAgentAsync(id, ct);
        if (agent is null) return Results.NotFound();
        return Results.Ok(new AgentDetailResponse(
            agent.Id, agent.Name, agent.Role, agent.Model, agent.Status.ToString(),
            agent.LastSeen, agent.Workspace, agent.AgentDir, agent.Description,
            agent.SubAgents, agent.IdentityName));
    }

    [HttpGet("{id}/activity")]
    [ProducesResponseType(typeof(IReadOnlyList<AgentActivityResponse>), StatusCodes.Status200OK)]
    public async Task<IResult> GetAgentActivity(string id, CancellationToken ct)
    {
        var items = await activityRepo.GetByAgentAsync(id, 50, ct);
        var activity = items
            .Select(x => new AgentActivityResponse(x.Id, x.Type, x.Message, x.CreatedAt, "activity"))
            .ToList();

        var gatewayEntries = await dashboardService.GetAgentActivityAsync(id, 10);
        foreach (var entry in gatewayEntries)
            activity.Add(new AgentActivityResponse(null, "thinking", entry.Text, entry.Timestamp, entry.Source, entry.Time));

        return Results.Ok(activity
            .OrderByDescending(x => x.At)
            .Take(50));
    }

    [HttpGet("{id}/summary")]
    [ProducesResponseType(typeof(AgentSummaryResponse), StatusCodes.Status200OK)]
    public async Task<IResult> GetAgentSummary(string id, CancellationToken ct)
    {
        var recent = await activityRepo.GetByAgentAsync(id, 25, ct);
        var gatewayEntries = await dashboardService.GetAgentActivityAsync(id, 8);
        return Results.Ok(AgentSummaryBuilder.Build(recent, gatewayEntries, DateTimeOffset.UtcNow));
    }

    [HttpPost("{id}/command")]
    [Authorize(Roles = "owner")]
    [EnableRateLimiting("agents")]
    [ProducesResponseType(typeof(AgentCommandResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status503ServiceUnavailable)]
    public async Task<IResult> SendCommand(string id, [FromBody] AgentCommandRequest request, CancellationToken ct)
    {
        var message = request.Message?.Trim();
        if (string.IsNullOrWhiteSpace(message) || message.Length > 8000)
            return Results.ValidationProblem(new Dictionary<string, string[]> { ["message"] = ["Message must contain between 1 and 8000 characters."] });

        var conversationId = $"nexus-command-{id}-{Guid.NewGuid():N}";

        try
        {
            var context = OpenClawInvocationContextFactory.Create(
                User.FindFirst("sub")?.Value
                    ?? User.FindFirst(ClaimTypes.NameIdentifier)?.Value
                    ?? User.Identity?.Name,
                Request.Headers["Idempotency-Key"].FirstOrDefault(),
                Request.Headers["X-Correlation-ID"].FirstOrDefault()
                    ?? HttpContext.TraceIdentifier,
                Request.Headers["traceparent"].FirstOrDefault()
                    ?? Activity.Current?.Id);
            var result = await chat.SendAsync(
                message,
                conversationId,
                id,
                new Models.OpenClawInvocationMetadata(
                    context.IdempotencyKey,
                    context.CorrelationId,
                    context.Actor,
                    context.TraceParent),
                ct);
            await activityRepo.AddAsync(new Data.ActivityEvent
            {
                Type = "agent",
                Message = $"Command dispatched to agent {id} as durable run {result.RunId}"
            }, ct);
            return Results.Ok(new AgentCommandResponse(result.Runtime, result.AgentId, result.ConversationId, result.Content));
        }
        catch (Exception exception)
        {
            logger.LogWarning(exception, "Agent command failed for {AgentId}", id);
            return Results.Problem(
                title: "Agent command failed",
                detail: $"Could not send command to agent {id}: {exception.Message}",
                statusCode: StatusCodes.Status503ServiceUnavailable);
        }
    }

    // ── Config Editor ──

    [HttpGet("{id}/config")]
    [Authorize(Roles = "owner")]
    public async Task<IResult> GetConfig(string id, CancellationToken ct)
    {
        var files = await agentConfiguration.GetAgentFilesAsync(id, ct);
        return Results.Ok(files.Files.Select(file => new
        {
            FileName = file.Name,
            file.Size,
            ModifiedAt = file.UpdatedAt,
            file.Missing,
            file.ContentHash
        }));
    }

    [HttpGet("{id}/config/{fileName}")]
    [Authorize(Roles = "owner")]
    public async Task<IResult> GetConfigFile(string id, string fileName, CancellationToken ct)
    {
        var file = await agentConfiguration.GetAgentFileAsync(id, fileName, ct);
        return file.Missing
            ? Results.NotFound()
            : Results.Ok(new
            {
                FileName = file.Name,
                file.Content,
                file.Size,
                ModifiedAt = file.UpdatedAt,
                file.ContentHash
            });
    }

    [HttpPut("{id}/config/{fileName}")]
    [Authorize(Roles = "owner")]
    public async Task<IResult> SaveConfigFile(string id, string fileName, [FromBody] SaveConfigRequest request, CancellationToken ct)
    {
        if (request.Content is null)
            return Results.ValidationProblem(new Dictionary<string, string[]>
            {
                ["content"] = ["Content is required."]
            });
        if (string.IsNullOrWhiteSpace(request.ExpectedHash))
            return Results.ValidationProblem(new Dictionary<string, string[]>
            {
                ["expectedHash"] = ["ExpectedHash is required."]
            });

        try
        {
            var caller = DescribeCaller(HttpContext.User);
            var idempotencyKey = Request.Headers["Idempotency-Key"].FirstOrDefault()?.Trim();
            if (string.IsNullOrWhiteSpace(idempotencyKey))
            {
                return Results.ValidationProblem(new Dictionary<string, string[]>
                {
                    ["Idempotency-Key"] = ["Idempotency-Key header is required."]
                });
            }

            var invocation = OpenClawInvocationContext.Create(
                caller,
                idempotencyKey,
                Request.Headers["X-Correlation-ID"].FirstOrDefault(),
                Request.Headers["traceparent"].FirstOrDefault());
            var result = await agentConfiguration.SetAgentFileAsync(
                id,
                fileName,
                new Nexus.Api.Models.UpdateOpenClawAgentFileRequest(request.Content, request.ExpectedHash),
                invocation,
                ct);

            await activityRepo.AddAsync(new Data.ActivityEvent
            {
                Type = "config_audit",
                Message = $"Config save agent={id} file={fileName} caller={caller} verified={result.Verified} state={result.State}",
            }, ct);

            return Results.Ok(new
            {
                FileName = result.File.Name,
                result.File.Size,
                ModifiedAt = result.File.UpdatedAt,
                result.File.ContentHash,
                result.Verified,
                result.State,
                result.Message
            });
        }
        catch (OpenClawAgentConfigurationConflictException ex)
        {
            return NexusHttpResults.Problem(
                StatusCodes.Status409Conflict,
                ex.Message,
                NexusProblemCodes.Conflict,
                extensions: new Dictionary<string, object?>
                {
                    ["expectedHash"] = ex.ExpectedHash,
                    ["currentHash"] = ex.CurrentHash
                });
        }
        catch (OpenClawAgentConfigurationValidationException ex)
        {
            await activityRepo.AddAsync(new Data.ActivityEvent
            {
                Type = "config_audit",
                Message = $"Config save rejected agent={id} file={fileName} caller={DescribeCaller(HttpContext.User)} validation=failed code=validation_failed",
            }, ct);
            return Results.ValidationProblem(new Dictionary<string, string[]>
            {
                [ex.Field] = [ex.Message]
            });
        }
    }

    private static string DescribeCaller(ClaimsPrincipal user)
    {
        var subject = user.FindFirst(ClaimTypes.NameIdentifier)?.Value
            ?? user.FindFirst(ClaimTypes.Email)?.Value
            ?? user.Identity?.Name
            ?? "unknown";

        var role = user.FindFirst(ClaimTypes.Role)?.Value ?? "owner";
        return $"{role}:{subject}".ToLowerInvariant();
    }
}

public sealed record AgentActivityResponse(
    long? Id,
    string Type,
    string Message,
    DateTimeOffset At,
    string Source,
    string? RelativeTime = null
);

public sealed record AgentSummaryResponse(
    AgentSummaryItemResponse Now,
    AgentSummaryItemResponse Today,
    DateTimeOffset GeneratedAt
);

public sealed record AgentSummaryItemResponse(
    string Text,
    string Source,
    DateTimeOffset? Timestamp
);

public static class AgentSummaryBuilder
{
    public static AgentSummaryResponse Build(
        IReadOnlyList<Nexus.Api.Data.ActivityEvent> activity,
        IReadOnlyList<Nexus.Api.Models.AgentActivityEntry> gatewayEntries,
        DateTimeOffset nowUtc)
    {
        var points = activity
            .Select(entry => new SummaryPoint(entry.Message, entry.CreatedAt, "nexus-activity"))
            .Concat(gatewayEntries.Select(entry => new SummaryPoint(entry.Text, entry.Timestamp, entry.Source)))
            .Select(point => point with { Text = AgentActivityText.RedactForDisplay(point.Text) })
            .Where(point => !string.IsNullOrWhiteSpace(point.Text))
            .OrderByDescending(point => point.Timestamp)
            .ToList();

        var current = points.FirstOrDefault();
        var now = current is null
            ? new AgentSummaryItemResponse("Keine aktuelle Aktivitaet.", "none", null)
            : new AgentSummaryItemResponse(current.Text, current.Source, current.Timestamp);

        var windowStart = nowUtc.AddHours(-24);
        var todayPoints = points
            .Where(point => point.Timestamp >= windowStart)
            .ToList();

        AgentSummaryItemResponse today;
        if (todayPoints.Count == 0)
        {
            today = new AgentSummaryItemResponse("Heute keine verwertbaren Checkpoints.", "none", null);
        }
        else
        {
            var snippets = todayPoints
                .Select(point => point.Text)
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .Take(3)
                .ToList();

            var extraCount = Math.Max(0, todayPoints.Count - snippets.Count);
            var text = $"Letzte 24h: {string.Join(" | ", snippets)}";
            if (extraCount > 0)
                text += $" (+{extraCount} weitere)";

            var source = todayPoints.Select(point => point.Source).Distinct(StringComparer.OrdinalIgnoreCase).Count() == 1
                ? todayPoints[0].Source
                : "derived-mixed";

            today = new AgentSummaryItemResponse(text, source, todayPoints[0].Timestamp);
        }

        return new AgentSummaryResponse(now, today, nowUtc);
    }

    private sealed record SummaryPoint(string Text, DateTimeOffset Timestamp, string Source);
}
