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

namespace Nexus.Api.Controllers;

/// <summary>
/// Authenticated, browser-safe OpenClaw control-plane facade.
/// Gateway credentials and raw protocol payloads remain inside the backend.
/// </summary>
[Authorize]
[ApiController]
[Route("api/v1/openclaw")]
public sealed class OpenClawController(IOpenClawControlService openClaw) : ControllerBase
{
    [HttpGet("connection")]
    [ProducesResponseType(typeof(OpenClawConnectionDto), StatusCodes.Status200OK)]
    public IResult GetConnection()
        => Results.Ok(openClaw.GetConnection());

    [HttpGet("capabilities")]
    [ProducesResponseType(typeof(IReadOnlyList<OpenClawCapabilityDto>), StatusCodes.Status200OK)]
    public IResult GetCapabilities()
        => Results.Ok(openClaw.GetCapabilities());

    [HttpGet("overview")]
    [ProducesResponseType(typeof(OpenClawOverviewDto), StatusCodes.Status200OK)]
    public async Task<IResult> GetOverview(CancellationToken cancellationToken)
        => Results.Ok(await openClaw.GetOverviewAsync(cancellationToken));

    [HttpGet("tasks")]
    [ProducesResponseType(typeof(OpenClawCollectionDto<OpenClawTaskDto>), StatusCodes.Status200OK)]
    public async Task<IResult> GetTasks(
        [FromQuery] int limit = 100,
        [FromQuery] string? cursor = null,
        CancellationToken cancellationToken = default)
        => Results.Ok(await openClaw.GetTasksAsync(limit, cursor, cancellationToken));

    [HttpPost("tasks/{taskId}/cancel")]
    [Authorize(Roles = "owner")]
    [EnableRateLimiting("agents")]
    [ProducesResponseType(typeof(OpenClawOperationDto<OpenClawTaskDto>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    public async Task<IResult> CancelTask(
        string taskId,
        [FromBody] CancelOpenClawTaskRequest request,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(taskId))
            return Results.ValidationProblem(new Dictionary<string, string[]>
            {
                ["taskId"] = ["Task id is required."]
            });

        return Results.Ok(await openClaw.CancelTaskAsync(
            taskId,
            request.Reason,
            cancellationToken));
    }

    [HttpGet("sessions")]
    [ProducesResponseType(typeof(OpenClawCollectionDto<OpenClawSessionDto>), StatusCodes.Status200OK)]
    public async Task<IResult> GetSessions(
        [FromQuery] int limit = 100,
        CancellationToken cancellationToken = default)
        => Results.Ok(await openClaw.GetSessionsAsync(limit, cancellationToken));

    [HttpPost("sessions/abort")]
    [Authorize(Roles = "owner")]
    [EnableRateLimiting("agents")]
    [ProducesResponseType(typeof(OpenClawOperationDto<object>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    public async Task<IResult> AbortSession(
        [FromBody] AbortOpenClawSessionRequest request,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(request.SessionKey))
            return Results.ValidationProblem(new Dictionary<string, string[]>
            {
                ["sessionKey"] = ["Session key is required."]
            });

        return Results.Ok(await openClaw.AbortSessionAsync(
            request.SessionKey,
            request.RunId,
            request.ClearQueued,
            cancellationToken));
    }

    [HttpPost("sessions/model")]
    [Authorize(Roles = "owner")]
    [EnableRateLimiting("agents")]
    [ProducesResponseType(typeof(OpenClawOperationDto<object>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    public async Task<IResult> PatchSessionModel(
        [FromBody] PatchOpenClawSessionModelRequest request,
        CancellationToken cancellationToken)
    {
        var errors = new Dictionary<string, string[]>();
        if (string.IsNullOrWhiteSpace(request.SessionKey))
            errors["sessionKey"] = ["Session key is required."];
        if (string.IsNullOrWhiteSpace(request.Model))
            errors["model"] = ["Model is required."];
        if (errors.Count > 0)
            return Results.ValidationProblem(errors);

        return Results.Ok(await openClaw.PatchSessionModelAsync(
            request.SessionKey,
            request.Model,
            cancellationToken));
    }

    [HttpGet("cron")]
    [ProducesResponseType(typeof(OpenClawCollectionDto<OpenClawCronJobDto>), StatusCodes.Status200OK)]
    public async Task<IResult> GetCronJobs(
        [FromQuery] bool includeDisabled = true,
        [FromQuery] int limit = 100,
        [FromQuery] string? cursor = null,
        CancellationToken cancellationToken = default)
        => Results.Ok(await openClaw.GetCronJobsAsync(
            includeDisabled,
            limit,
            cursor,
            cancellationToken));

    [HttpGet("cron/{jobId}")]
    [Authorize(Roles = "owner")]
    [ProducesResponseType(typeof(OpenClawOperationDto<OpenClawCronJobDetailDto>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(OpenClawOperationDto<OpenClawCronJobDetailDto>), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    public async Task<IResult> GetCronJob(
        string jobId,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(jobId))
            return MissingCronJobId();

        var result = await openClaw.GetCronJobAsync(jobId, cancellationToken);
        return result.State == "not_found"
            ? Results.NotFound(result)
            : Results.Ok(result);
    }

    [HttpGet("cron/{jobId}/runs")]
    [ProducesResponseType(typeof(OpenClawCollectionDto<OpenClawCronRunDto>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    public async Task<IResult> GetCronRuns(
        string jobId,
        [FromQuery] int limit = 100,
        [FromQuery] string? cursor = null,
        [FromQuery] string? runId = null,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(jobId))
            return MissingCronJobId();

        return Results.Ok(await openClaw.GetCronRunsAsync(
            jobId,
            limit,
            cursor,
            runId,
            cancellationToken));
    }

    [HttpPost("cron")]
    [Authorize(Roles = "owner")]
    [EnableRateLimiting("agents")]
    [ProducesResponseType(typeof(OpenClawOperationDto<OpenClawCronJobDetailDto>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(OpenClawOperationDto<OpenClawCronJobDetailDto>), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(OpenClawOperationDto<OpenClawCronJobDetailDto>), StatusCodes.Status403Forbidden)]
    [ProducesResponseType(typeof(OpenClawOperationDto<OpenClawCronJobDetailDto>), StatusCodes.Status409Conflict)]
    public async Task<IResult> CreateCronJob(
        [FromBody] CreateOpenClawCronJobRequest request,
        CancellationToken cancellationToken)
    {
        var headerError = RequireIdempotencyKey();
        if (headerError is not null)
            return headerError;

        return CronMutationResult(await openClaw.CreateCronJobAsync(
            request,
            cancellationToken));
    }

    [HttpPatch("cron/{jobId}")]
    [Authorize(Roles = "owner")]
    [EnableRateLimiting("agents")]
    [ProducesResponseType(typeof(OpenClawOperationDto<OpenClawCronJobDetailDto>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(OpenClawOperationDto<OpenClawCronJobDetailDto>), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(OpenClawOperationDto<OpenClawCronJobDetailDto>), StatusCodes.Status403Forbidden)]
    [ProducesResponseType(typeof(OpenClawOperationDto<OpenClawCronJobDetailDto>), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(OpenClawOperationDto<OpenClawCronJobDetailDto>), StatusCodes.Status409Conflict)]
    public async Task<IResult> PatchCronJob(
        string jobId,
        [FromBody] PatchOpenClawCronJobRequest request,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(jobId))
            return MissingCronJobId();
        if (request.Patch is null)
            return Results.ValidationProblem(new Dictionary<string, string[]>
            {
                ["patch"] = ["Cron patch is required."]
            });
        var headerError = RequireIdempotencyKey();
        if (headerError is not null)
            return headerError;
        var expectedHash = ResolveExpectedHash(request.ExpectedHash);
        if (string.IsNullOrWhiteSpace(expectedHash))
            return MissingExpectedHash();

        return CronMutationResult(await openClaw.PatchCronJobAsync(
            jobId,
            request.Patch,
            expectedHash,
            cancellationToken));
    }

    [HttpDelete("cron/{jobId}")]
    [Authorize(Roles = "owner")]
    [EnableRateLimiting("agents")]
    [ProducesResponseType(typeof(OpenClawOperationDto<object>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(OpenClawOperationDto<object>), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(OpenClawOperationDto<object>), StatusCodes.Status403Forbidden)]
    [ProducesResponseType(typeof(OpenClawOperationDto<object>), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(OpenClawOperationDto<object>), StatusCodes.Status409Conflict)]
    public async Task<IResult> DeleteCronJob(
        string jobId,
        [FromQuery] string? expectedHash = null,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(jobId))
            return MissingCronJobId();
        var headerError = RequireIdempotencyKey();
        if (headerError is not null)
            return headerError;
        expectedHash = ResolveExpectedHash(expectedHash);
        if (string.IsNullOrWhiteSpace(expectedHash))
            return MissingExpectedHash();

        return CronMutationResult(await openClaw.DeleteCronJobAsync(
            jobId,
            expectedHash,
            cancellationToken));
    }

    [HttpPost("cron/{jobId}/run")]
    [Authorize(Roles = "owner")]
    [EnableRateLimiting("agents")]
    [ProducesResponseType(typeof(OpenClawOperationDto<object>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(OpenClawOperationDto<object>), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(OpenClawOperationDto<object>), StatusCodes.Status403Forbidden)]
    [ProducesResponseType(typeof(OpenClawOperationDto<object>), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(OpenClawOperationDto<object>), StatusCodes.Status409Conflict)]
    public async Task<IResult> RunCronJob(
        string jobId,
        [FromQuery] string? expectedHash = null,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(jobId))
            return MissingCronJobId();
        var headerError = RequireIdempotencyKey();
        if (headerError is not null)
            return headerError;
        expectedHash = ResolveExpectedHash(expectedHash);
        if (string.IsNullOrWhiteSpace(expectedHash))
            return MissingExpectedHash();

        return CronMutationResult(await openClaw.RunCronJobAsync(
            jobId,
            expectedHash,
            cancellationToken));
    }

    [HttpGet("approvals")]
    [ProducesResponseType(typeof(OpenClawCollectionDto<OpenClawApprovalDto>), StatusCodes.Status200OK)]
    public async Task<IResult> GetApprovals(
        [FromQuery] int limit = 100,
        CancellationToken cancellationToken = default)
        => Results.Ok(await openClaw.GetApprovalsAsync(limit, cancellationToken));

    [HttpPost("approvals/{approvalId}/resolve")]
    [Authorize(Roles = "owner")]
    [EnableRateLimiting("agents")]
    [ProducesResponseType(typeof(OpenClawOperationDto<OpenClawApprovalDto>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    public async Task<IResult> ResolveApproval(
        string approvalId,
        [FromBody] ResolveOpenClawApprovalRequest request,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(approvalId))
            return Results.ValidationProblem(new Dictionary<string, string[]>
            {
                ["approvalId"] = ["Approval id is required."]
            });
        if (string.IsNullOrWhiteSpace(request.Kind))
            return Results.ValidationProblem(new Dictionary<string, string[]>
            {
                ["kind"] = ["Approval kind is required."]
            });

        return Results.Ok(await openClaw.ResolveApprovalAsync(
            approvalId,
            request.Kind,
            request.Decision,
            cancellationToken));
    }

    [HttpGet("activity")]
    [ProducesResponseType(typeof(OpenClawCollectionDto<OpenClawActivityDto>), StatusCodes.Status200OK)]
    public async Task<IResult> GetActivity(
        [FromQuery] int limit = 100,
        [FromQuery] string? cursor = null,
        CancellationToken cancellationToken = default)
        => Results.Ok(await openClaw.GetActivityAsync(limit, cursor, cancellationToken));

    [HttpGet("models")]
    [ProducesResponseType(typeof(OpenClawCollectionDto<OpenClawModelDto>), StatusCodes.Status200OK)]
    public async Task<IResult> GetModels(CancellationToken cancellationToken)
        => Results.Ok(await openClaw.GetModelsAsync(cancellationToken));

    [HttpGet("models/auth-status")]
    [ProducesResponseType(typeof(OpenClawCollectionDto<OpenClawModelAuthProviderDto>), StatusCodes.Status200OK)]
    public async Task<IResult> GetModelAuthStatus(
        [FromQuery] bool refresh = false,
        CancellationToken cancellationToken = default)
        => Results.Ok(await openClaw.GetModelAuthStatusAsync(refresh, cancellationToken));

    [HttpGet("agents")]
    [ProducesResponseType(typeof(OpenClawCollectionDto<OpenClawAgentDto>), StatusCodes.Status200OK)]
    public async Task<IResult> GetAgents(CancellationToken cancellationToken)
        => Results.Ok(await openClaw.GetAgentsAsync(cancellationToken));

    private string? ResolveExpectedHash(string? value)
    {
        if (!string.IsNullOrWhiteSpace(value))
            return value;
        return Request.Headers.IfMatch.FirstOrDefault();
    }

    private static IResult MissingCronJobId()
        => Results.ValidationProblem(new Dictionary<string, string[]>
        {
            ["jobId"] = ["Cron job id is required."]
        });

    private static IResult MissingExpectedHash()
        => Results.ValidationProblem(new Dictionary<string, string[]>
        {
            ["expectedHash"] =
            [
                "A current OpenClaw resource hash is required for this cron mutation."
            ]
        });

    private IResult? RequireIdempotencyKey()
    {
        var value = Request.Headers["Idempotency-Key"].FirstOrDefault()?.Trim();
        if (!string.IsNullOrWhiteSpace(value) &&
            value.Length <= 128 &&
            !value.Any(char.IsControl))
        {
            return null;
        }

        return Results.ValidationProblem(new Dictionary<string, string[]>
        {
            ["Idempotency-Key"] =
            [
                "A non-empty Idempotency-Key header with at most 128 non-control characters is required."
            ]
        });
    }

    private static IResult CronMutationResult<T>(OpenClawOperationDto<T> result)
    {
        return result.State switch
        {
            "conflict" => Results.Json(result, statusCode: StatusCodes.Status409Conflict),
            "not_found" => Results.Json(result, statusCode: StatusCodes.Status404NotFound),
            "invalid" => Results.Json(result, statusCode: StatusCodes.Status400BadRequest),
            "restricted" or "management_disabled" or "forbidden" =>
                Results.Json(result, statusCode: StatusCodes.Status403Forbidden),
            _ => Results.Ok(result)
        };
    }
}
