using System.Globalization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Security;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Audit;

/// <summary>
/// Minimal-API endpoint hosting the Audit Log CSV export.
///
/// <para>
/// CentralUI ships no MVC controllers (see <see cref="ZB.MOM.WW.ScadaBridge.CentralUI.Auth.AuthEndpoints"/>
/// and <see cref="ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.ScriptAnalysisEndpoints"/>),
/// so the brief's "controller" is implemented as a minimal-API endpoint instead.
/// The endpoint streams to <c>Response.Body</c> directly so the export does NOT
/// buffer the full result set in memory — see
/// <see cref="IAuditLogExportService.ExportAsync"/>.
/// </para>
///
/// <para>
/// The route is gated on the <see cref="AuthorizationPolicies.AuditExport"/>
/// policy so only roles with the bulk-export
/// permission can pull a CSV — the page-level
/// <see cref="AuthorizationPolicies.OperationalAudit"/> gate is read-only
/// and intentionally narrower. The query-string parser silently drops
/// unrecognised values to match the page-level parser in
/// <c>AuditLogPage.ApplyQueryStringFilters</c> — an unknown enum value yields
/// the same "no constraint" outcome rather than a 400.
/// </para>
/// </summary>
public static class AuditExportEndpoints
{
    /// <summary>
    /// Default row cap for a single export. Large enough to satisfy realistic
    /// operator workflows; mirrors the brief's recommended ceiling. Operators
    /// who need more should fall back to the CLI (footnote rendered in the
    /// cap-footer line).
    /// </summary>
    public const int DefaultMaxRows = 100_000;

    /// <summary>Registers the audit log CSV export endpoint on the given route builder.</summary>
    /// <param name="endpoints">The endpoint route builder to register against.</param>
    /// <returns>The same <paramref name="endpoints"/> instance for chaining.</returns>
    public static IEndpointRouteBuilder MapAuditExportEndpoints(this IEndpointRouteBuilder endpoints)
    {
        endpoints.MapGet("/api/centralui/audit/export", HandleExportAsync)
            .RequireAuthorization(AuthorizationPolicies.AuditExport);

        return endpoints;
    }

    /// <summary>
    /// Handles <c>GET /api/centralui/audit/export</c>. Internal so endpoint
    /// tests can call it directly when desirable; the live wire-up goes
    /// through the minimal-API map above.
    /// </summary>
    /// <param name="context">The HTTP context for the current request.</param>
    /// <param name="exportService">The export service used to stream audit rows as CSV.</param>
    /// <returns>A task representing the asynchronous export streaming operation.</returns>
    internal static async Task HandleExportAsync(HttpContext context, IAuditLogExportService exportService)
    {
        var filter = ParseFilter(context.Request.Query);
        var maxRows = ParseMaxRows(context.Request.Query);

        // Stamp the response headers BEFORE the first body write so the client
        // sees text/csv + an attachment download right away.
        var fileName = $"audit-log-{DateTime.UtcNow:yyyyMMdd-HHmmss}.csv";
        context.Response.ContentType = "text/csv; charset=utf-8";
        context.Response.Headers["Content-Disposition"] = $"attachment; filename=\"{fileName}\"";
        // Defeat any intermediate buffering proxy so the operator sees rows
        // streaming through as the server flushes each repository page.
        context.Response.Headers["Cache-Control"] = "no-store";

        await exportService.ExportAsync(filter, maxRows, context.Response.Body, context.RequestAborted);
    }

    /// <summary>
    /// Parses the query-string into an <see cref="AuditLogQueryFilter"/>. The
    /// <c>channel</c>/<c>kind</c>/<c>status</c>/<c>site</c> dimensions are
    /// multi-value: a repeated query param yields a multi-element filter list, a
    /// single param a one-element list. Unknown enum names / un-parseable Guids /
    /// dates are silently dropped (same lax contract as
    /// <c>AuditLogPage.ApplyQueryStringFilters</c>) — an unparseable value within
    /// a repeated set is dropped, not the whole set.
    /// </summary>
    /// <remarks>
    /// This endpoint reads the source-site filter from the <c>site</c> query key,
    /// whereas the ManagementService export endpoint reads it as
    /// <c>sourceSiteId</c>. The divergence is deliberate — each endpoint matches
    /// its own CLI / UI URL builder — so do NOT "fix" the two to one key name.
    /// </remarks>
    /// <param name="query">The query string parameters from the HTTP request.</param>
    /// <returns>An <see cref="AuditLogQueryFilter"/> populated from the query string values.</returns>
    internal static AuditLogQueryFilter ParseFilter(IQueryCollection query)
    {
        var channels = AuditQueryParamParsers.ParseEnumList<AuditChannel>(query["channel"]);
        var kinds = AuditQueryParamParsers.ParseEnumList<AuditKind>(query["kind"]);
        var statuses = AuditQueryParamParsers.ParseEnumList<AuditStatus>(query["status"]);
        var sites = AuditQueryParamParsers.ParseStringList(query["site"]);

        string? target = TrimToNullable(query, "target");
        string? actor = TrimToNullable(query, "actor");

        Guid? correlationId = null;
        if (query.TryGetValue("correlationId", out var corrValues)
            && Guid.TryParse(corrValues.ToString(), out var parsedCorr))
        {
            correlationId = parsedCorr;
        }

        Guid? executionId = null;
        if (query.TryGetValue("executionId", out var execValues)
            && Guid.TryParse(execValues.ToString(), out var parsedExec))
        {
            executionId = parsedExec;
        }

        Guid? parentExecutionId = null;
        if (query.TryGetValue("parentExecutionId", out var parentExecValues)
            && Guid.TryParse(parentExecValues.ToString(), out var parsedParentExec))
        {
            parentExecutionId = parsedParentExec;
        }

        DateTime? fromUtc = ParseUtcDate(query, "from");
        DateTime? toUtc = ParseUtcDate(query, "to");

        return new AuditLogQueryFilter(
            Channels: channels,
            Kinds: kinds,
            Statuses: statuses,
            SourceSiteIds: sites,
            Target: target,
            Actor: actor,
            CorrelationId: correlationId,
            ExecutionId: executionId,
            ParentExecutionId: parentExecutionId,
            FromUtc: fromUtc,
            ToUtc: toUtc);
    }

    /// <summary>
    /// Optional <c>maxRows=</c> query-string override. Falls back to
    /// <see cref="DefaultMaxRows"/> on a missing / non-positive / unparseable
    /// value rather than erroring — same lax contract as the rest of the
    /// query parser.
    /// </summary>
    private static int ParseMaxRows(IQueryCollection query)
    {
        if (query.TryGetValue("maxRows", out var raw)
            && int.TryParse(raw.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
            && parsed > 0)
        {
            return parsed;
        }
        return DefaultMaxRows;
    }

    private static string? TrimToNullable(IQueryCollection query, string key)
    {
        if (!query.TryGetValue(key, out var values))
        {
            return null;
        }
        var v = values.ToString();
        return string.IsNullOrWhiteSpace(v) ? null : v.Trim();
    }

    private static DateTime? ParseUtcDate(IQueryCollection query, string key)
    {
        if (!query.TryGetValue(key, out var values))
        {
            return null;
        }
        if (DateTime.TryParse(
            values.ToString(),
            CultureInfo.InvariantCulture,
            DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
            out var parsed))
        {
            return DateTime.SpecifyKind(parsed, DateTimeKind.Utc);
        }
        return null;
    }
}
