using System.ComponentModel.DataAnnotations;
using Jiaowu.Api.Contracts;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Domain.System;
using Jiaowu.Api.Infrastructure.Operations;
using Jiaowu.Api.Infrastructure.Observability;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Controllers;

[ApiController]
[Authorize(Roles = SystemRoles.SuperAdmin)]
[Route("api/operations")]
public sealed class OperationsController(
    AppDbContext db,
    OperationalHealthService healthService,
    DatabaseBackupService backupService,
    PerformanceReportService performanceReportService,
    OperationsOptions options) : ControllerBase
{
    [HttpGet("performance")]
    public async Task<ActionResult<PerformanceReport>> GetPerformance(
        [FromQuery] string? range = "1h",
        CancellationToken cancellationToken = default)
    {
        try
        {
            return Ok(await performanceReportService.GetAsync(
                range,
                cancellationToken));
        }
        catch (ArgumentOutOfRangeException)
        {
            return ValidationProblem(
                "性能报表范围仅支持 15m、1h、24h 或 7d。");
        }
    }

    [HttpGet("summary")]
    public async Task<ActionResult<OperationsSummary>> GetSummary(
        CancellationToken cancellationToken)
    {
        var since = DateTime.UtcNow.AddHours(-24);
        var health = await healthService.CheckAsync(cancellationToken);
        var backups = await backupService.ListAsync(cancellationToken);
        var auditCount = await db.AuditLogs.AsNoTracking()
            .CountAsync(x => x.CreatedAt >= since, cancellationToken);
        var serverErrorCount = await db.AuditLogs.AsNoTracking()
            .CountAsync(
                x => x.CreatedAt >= since && x.StatusCode >= 500,
                cancellationToken);
        var failedJobCount = await CountFailedJobsAsync(
            DateTime.UtcNow.AddDays(-7),
            cancellationToken);
        var alerts = await BuildAlertsAsync(
            health,
            backups,
            serverErrorCount,
            failedJobCount,
            cancellationToken);
        return Ok(new OperationsSummary(
            DateTime.UtcNow,
            health,
            new OperationsCounters(
                auditCount,
                serverErrorCount,
                failedJobCount,
                alerts.Count(x => x.Severity == "critical")),
            alerts,
            backups.FirstOrDefault()));
    }

    [HttpGet("health")]
    public async Task<ActionResult<OperationalHealthSnapshot>> GetHealth(
        CancellationToken cancellationToken) =>
        Ok(await healthService.CheckAsync(cancellationToken));

    [HttpGet("swagger")]
    public async Task<ActionResult<SwaggerDocumentationSettings>> GetSwaggerSettings(
        CancellationToken cancellationToken) =>
        Ok(new SwaggerDocumentationSettings(await IsSwaggerEnabledAsync(cancellationToken)));

    [HttpPut("swagger")]
    public async Task<ActionResult<SwaggerDocumentationSettings>> UpdateSwaggerSettings(
        UpdateSwaggerDocumentationSettings request,
        CancellationToken cancellationToken)
    {
        var setting = await db.SystemFeatureSettings.SingleOrDefaultAsync(
            x => x.Key == SystemFeatureKeys.SwaggerDocumentation,
            cancellationToken);
        if (setting is null)
        {
            setting = new SystemFeatureSetting
            {
                Key = SystemFeatureKeys.SwaggerDocumentation,
                IsEnabled = request.IsEnabled
            };
            db.SystemFeatureSettings.Add(setting);
        }
        else
        {
            setting.IsEnabled = request.IsEnabled;
            setting.UpdatedAt = DateTime.UtcNow;
        }

        await db.SaveChangesAsync(cancellationToken);
        return Ok(new SwaggerDocumentationSettings(setting.IsEnabled));
    }

    [HttpGet("audit-logs")]
    public async Task<ActionResult<PagedResult<AuditLogItem>>> GetAuditLogs(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        [FromQuery] string? method = null,
        [FromQuery] int? statusCode = null,
        [FromQuery] string? userName = null,
        [FromQuery] string? path = null,
        [FromQuery] DateTime? from = null,
        [FromQuery] DateTime? to = null,
        CancellationToken cancellationToken = default)
    {
        var pagingError = ValidatePaging(page, pageSize);
        if (pagingError is not null) return pagingError;
        var rangeError = ValidateRange(from, to);
        if (rangeError is not null) return rangeError;

        var query = db.AuditLogs.AsNoTracking().AsQueryable();
        if (!string.IsNullOrWhiteSpace(method))
        {
            var normalizedMethod = method.Trim().ToUpperInvariant();
            query = query.Where(x => x.Method == normalizedMethod);
        }
        if (statusCode.HasValue)
            query = query.Where(x => x.StatusCode == statusCode.Value);
        if (!string.IsNullOrWhiteSpace(userName))
        {
            var normalizedUser = userName.Trim();
            query = query.Where(x =>
                x.UserName != null && x.UserName.Contains(normalizedUser));
        }
        if (!string.IsNullOrWhiteSpace(path))
        {
            var normalizedPath = path.Trim();
            query = query.Where(x => x.Path.Contains(normalizedPath));
        }
        query = query.Where(x =>
            x.CreatedAt >= (from ?? DateTime.UtcNow.AddDays(-1)));
        if (to.HasValue)
            query = query.Where(x => x.CreatedAt <= to.Value);

        var total = await query.CountAsync(cancellationToken);
        var items = await query
            .OrderByDescending(x => x.CreatedAt)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(x => new AuditLogItem(
                x.Id,
                x.UserName,
                x.Method,
                x.Path,
                x.StatusCode,
                x.IpAddress,
                x.CreatedAt))
            .ToListAsync(cancellationToken);
        return Ok(new PagedResult<AuditLogItem>(items, total, page, pageSize));
    }

    [HttpGet("failed-jobs")]
    public async Task<ActionResult<PagedResult<FailedBackgroundJobItem>>>
        GetFailedJobs(
            [FromQuery] int page = 1,
            [FromQuery] int pageSize = 20,
            [FromQuery] string? kind = null,
            [FromQuery] DateTime? from = null,
            [FromQuery] DateTime? to = null,
            CancellationToken cancellationToken = default)
    {
        var pagingError = ValidatePaging(page, pageSize);
        if (pagingError is not null) return pagingError;
        var rangeError = ValidateRange(from, to);
        if (rangeError is not null) return rangeError;

        var normalizedKind = NormalizeJobKind(kind);
        if (kind is not null && normalizedKind is null)
            return ValidationProblem("后台任务类型无效。");

        var effectiveFrom = from ?? DateTime.UtcNow.AddDays(-30);
        var take = checked(page * pageSize);
        var rows = new List<FailedBackgroundJobItem>();
        var total = 0;

        if (normalizedKind is null or "AutomaticSchedule")
        {
            var query = db.AutomaticScheduleJobs.AsNoTracking()
                .Where(x =>
                    x.Status == AutomaticScheduleJobStatus.Failed &&
                    x.CreatedAt >= effectiveFrom);
            if (to.HasValue) query = query.Where(x => x.CreatedAt <= to.Value);
            total += await query.CountAsync(cancellationToken);
            rows.AddRange(await query
                .OrderByDescending(x => x.CompletedAt ?? x.UpdatedAt)
                .Take(take)
                .Select(x => new FailedBackgroundJobItem(
                    x.Id,
                    "AutomaticSchedule",
                    "自动排课",
                    x.SchedulePlan == null ? "排课方案" : x.SchedulePlan.Name,
                    x.ErrorMessage ?? "任务失败但未记录错误详情。",
                    x.CreatedAt,
                    x.StartedAt,
                    x.CompletedAt,
                    0))
                .ToListAsync(cancellationToken));
        }

        if (normalizedKind is null or "SchedulePublish")
        {
            var query = db.SchedulePublishJobs.AsNoTracking()
                .Where(x =>
                    x.Status == SchedulePublishJobStatus.Failed &&
                    x.CreatedAt >= effectiveFrom);
            if (to.HasValue) query = query.Where(x => x.CreatedAt <= to.Value);
            total += await query.CountAsync(cancellationToken);
            rows.AddRange(await query
                .OrderByDescending(x => x.CompletedAt ?? x.UpdatedAt)
                .Take(take)
                .Select(x => new FailedBackgroundJobItem(
                    x.Id,
                    "SchedulePublish",
                    "课表发布",
                    x.SchedulePlan == null ? "排课方案" : x.SchedulePlan.Name,
                    x.ErrorMessage ?? "任务失败但未记录错误详情。",
                    x.CreatedAt,
                    x.StartedAt,
                    x.CompletedAt,
                    0))
                .ToListAsync(cancellationToken));
        }

        if (normalizedKind is null or "MakeupExamAuto")
        {
            var query = db.MakeupExamAutoJobs.AsNoTracking()
                .Where(x =>
                    x.Status == MakeupExamAutoJobStatus.Failed &&
                    x.CreatedAt >= effectiveFrom);
            if (to.HasValue) query = query.Where(x => x.CreatedAt <= to.Value);
            total += await query.CountAsync(cancellationToken);
            rows.AddRange(await query
                .OrderByDescending(x => x.CompletedAt ?? x.UpdatedAt)
                .Take(take)
                .Select(x => new FailedBackgroundJobItem(
                    x.Id,
                    "MakeupExamAuto",
                    "补考自动安排",
                    x.MakeupExamPlan == null ? "补考计划" : x.MakeupExamPlan.Name,
                    x.ErrorMessage ?? "任务失败但未记录错误详情。",
                    x.CreatedAt,
                    x.StartedAt,
                    x.CompletedAt,
                    0))
                .ToListAsync(cancellationToken));
        }

        if (normalizedKind is null or "ExamArrangement")
        {
            var query = db.ExamArrangementJobs.AsNoTracking()
                .Where(x =>
                    x.Status == ExamArrangementJobStatus.Failed &&
                    x.CreatedAt >= effectiveFrom);
            if (to.HasValue) query = query.Where(x => x.CreatedAt <= to.Value);
            total += await query.CountAsync(cancellationToken);
            rows.AddRange(await query
                .OrderByDescending(x => x.CompletedAt ?? x.UpdatedAt)
                .Take(take)
                .Select(x => new FailedBackgroundJobItem(
                    x.Id,
                    "ExamArrangement",
                    x.Kind == ExamArrangementKind.FormalExam
                        ? "正式考试编排"
                        : "补考编排",
                    x.Kind == ExamArrangementKind.FormalExam
                        ? "正式考试计划"
                        : "补考计划",
                    x.ErrorMessage ?? "任务失败但未记录错误详情。",
                    x.CreatedAt,
                    x.StartedAt,
                    x.CompletedAt,
                    0))
                .ToListAsync(cancellationToken));
        }

        if (normalizedKind is null or "ExamPublish")
        {
            var query = db.ExamPublishJobs.AsNoTracking()
                .Where(x =>
                    x.Status == ExamPublishJobStatus.Failed &&
                    x.CreatedAt >= effectiveFrom);
            if (to.HasValue) query = query.Where(x => x.CreatedAt <= to.Value);
            total += await query.CountAsync(cancellationToken);
            rows.AddRange(await query
                .OrderByDescending(x => x.CompletedAt ?? x.UpdatedAt)
                .Take(take)
                .Select(x => new FailedBackgroundJobItem(
                    x.Id,
                    "ExamPublish",
                    x.Kind == ExamPublishJobKind.FormalExam
                        ? "正式考试发布"
                        : "补考发布",
                    x.Kind == ExamPublishJobKind.FormalExam
                        ? "正式考试计划"
                        : "补考计划",
                    x.ErrorMessage ?? "任务失败但未记录错误详情。",
                    x.CreatedAt,
                    x.StartedAt,
                    x.CompletedAt,
                    0))
                .ToListAsync(cancellationToken));
        }

        var pageItems = rows
            .OrderByDescending(x => x.CompletedAt ?? x.CreatedAt)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToArray();
        if (pageItems.Length > 0)
        {
            var ids = pageItems.Select(x => x.Id).ToArray();
            var attempts = await db.BackgroundJobOutboxMessages.AsNoTracking()
                .WhereIn(ids, x => x.JobId)
                .Select(x => new { x.JobId, x.ProcessingAttempts })
                .ToDictionaryAsync(x => x.JobId, x => x.ProcessingAttempts,
                    cancellationToken);
            pageItems = pageItems
                .Select(x => x with
                {
                    ProcessingAttempts = attempts.GetValueOrDefault(x.Id)
                })
                .ToArray();
        }

        return Ok(new PagedResult<FailedBackgroundJobItem>(
            pageItems,
            total,
            page,
            pageSize));
    }

    [HttpGet("backups")]
    public async Task<ActionResult<IReadOnlyCollection<BackupArtifact>>> GetBackups(
        CancellationToken cancellationToken) =>
        Ok(await backupService.ListAsync(cancellationToken));

    [HttpPost("backups")]
    public async Task<ActionResult<BackupArtifact>> CreateBackup(
        CreateBackupRequest request,
        CancellationToken cancellationToken)
    {
        try
        {
            var artifact = await backupService.CreateAsync(
                request.Note,
                cancellationToken);
            return CreatedAtAction(
                nameof(GetBackups),
                new { id = artifact.Id },
                artifact);
        }
        catch (Exception exception) when (exception is not OperationCanceledException)
        {
            return Problem(
                title: "数据库备份失败",
                detail: SafeMessage(exception),
                statusCode: StatusCodes.Status503ServiceUnavailable);
        }
    }

    [HttpPost("backups/{backupId}/restore-drill")]
    public async Task<ActionResult<RestoreDrillResult>> RunRestoreDrill(
        string backupId,
        RestoreDrillRequest request,
        CancellationToken cancellationToken)
    {
        if (!request.Confirmation.Equals(
                "RESTORE_DRILL",
                StringComparison.Ordinal))
        {
            return ValidationProblem(
                "恢复演练必须明确确认，且不会覆盖当前业务数据库。");
        }

        try
        {
            return Ok(await backupService.RunRestoreDrillAsync(
                backupId,
                cancellationToken));
        }
        catch (FileNotFoundException)
        {
            return NotFound(new ProblemDetails
            {
                Title = "备份不存在",
                Detail = "指定备份不存在或其文件已被移除。",
                Status = StatusCodes.Status404NotFound
            });
        }
    }

    private async Task<IReadOnlyCollection<OperationalAlert>> BuildAlertsAsync(
        OperationalHealthSnapshot health,
        IReadOnlyCollection<BackupArtifact> backups,
        int serverErrorCount,
        int failedJobCount,
        CancellationToken cancellationToken)
    {
        var alerts = health.Components
            .Where(x => x.Status != "healthy")
            .Select(x => new OperationalAlert(
                $"health-{x.Key}",
                x.Status == "unhealthy" ? "critical" : "warning",
                "health",
                $"{x.Label}状态异常",
                x.Detail,
                health.CheckedAt))
            .ToList();

        if (serverErrorCount > 0)
        {
            var latest = await db.AuditLogs.AsNoTracking()
                .Where(x =>
                    x.CreatedAt >= DateTime.UtcNow.AddHours(-24) &&
                    x.StatusCode >= 500)
                .OrderByDescending(x => x.CreatedAt)
                .Select(x => new { x.Path, x.StatusCode, x.CreatedAt })
                .FirstOrDefaultAsync(cancellationToken);
            alerts.Add(new OperationalAlert(
                "http-5xx",
                "critical",
                "audit",
                $"过去 24 小时发生 {serverErrorCount} 次服务端错误",
                latest is null
                    ? "请检查服务日志定位异常。"
                    : $"最近一次为 {latest.StatusCode} {latest.Path}。",
                latest?.CreatedAt ?? DateTime.UtcNow));
        }

        if (failedJobCount > 0)
        {
            alerts.Add(new OperationalAlert(
                "failed-jobs",
                "critical",
                "jobs",
                $"最近 7 天有 {failedJobCount} 个后台任务失败",
                "任务已停止或达到重试上限，请在失败任务中查看错误详情。",
                DateTime.UtcNow));
        }

        var retryingFailures = await db.BackgroundJobOutboxMessages.AsNoTracking()
            .CountAsync(
                x => x.State != BackgroundJobOutboxState.Completed &&
                     x.LastError != null,
                cancellationToken);
        if (retryingFailures > 0)
        {
            alerts.Add(new OperationalAlert(
                "retrying-jobs",
                "warning",
                "jobs",
                $"{retryingFailures} 个后台任务正在错误重试",
                "任务队列仍会自动重试；若持续出现，请检查依赖服务与任务参数。",
                DateTime.UtcNow));
        }

        var latestBackup = backups.FirstOrDefault();
        if (latestBackup is null)
        {
            alerts.Add(new OperationalAlert(
                "backup-missing",
                "critical",
                "backup",
                "尚无可验证的数据库备份",
                "立即创建首个备份，并在创建后执行一次隔离恢复演练。",
                DateTime.UtcNow));
        }
        else
        {
            var ageHours = (DateTime.UtcNow - latestBackup.CreatedAt).TotalHours;
            if (ageHours > options.BackupWarningHours)
            {
                alerts.Add(new OperationalAlert(
                    "backup-stale",
                    "warning",
                    "backup",
                    $"最近备份已超过 {options.BackupWarningHours} 小时",
                    $"最近备份创建于 {latestBackup.CreatedAt:u}。",
                    latestBackup.CreatedAt));
            }
            if (latestBackup.LastDrillSucceeded == false)
            {
                alerts.Add(new OperationalAlert(
                    "restore-drill-failed",
                    "critical",
                    "backup",
                    "最近一次恢复演练失败",
                    latestBackup.LastDrillDetail ?? "请重新运行演练并检查数据库工具日志。",
                    latestBackup.LastDrillAt ?? latestBackup.CreatedAt));
            }
            else if (!latestBackup.LastDrillAt.HasValue)
            {
                alerts.Add(new OperationalAlert(
                    "restore-drill-missing",
                    "warning",
                    "backup",
                    "最近备份尚未完成恢复演练",
                    "恢复演练只写入隔离数据库，不会覆盖当前业务数据。",
                    latestBackup.CreatedAt));
            }
        }

        return alerts
            .OrderBy(x => x.Severity == "critical" ? 0 : 1)
            .ThenByDescending(x => x.OccurredAt)
            .ToArray();
    }

    private async Task<int> CountFailedJobsAsync(
        DateTime from,
        CancellationToken cancellationToken) =>
        await db.AutomaticScheduleJobs.AsNoTracking()
            .CountAsync(
                x => x.Status == AutomaticScheduleJobStatus.Failed &&
                     x.CreatedAt >= from,
                cancellationToken) +
        await db.SchedulePublishJobs.AsNoTracking()
            .CountAsync(
                x => x.Status == SchedulePublishJobStatus.Failed &&
                     x.CreatedAt >= from,
                cancellationToken) +
        await db.MakeupExamAutoJobs.AsNoTracking()
            .CountAsync(
                x => x.Status == MakeupExamAutoJobStatus.Failed &&
                     x.CreatedAt >= from,
                cancellationToken) +
        await db.ExamArrangementJobs.AsNoTracking()
            .CountAsync(
                x => x.Status == ExamArrangementJobStatus.Failed &&
                     x.CreatedAt >= from,
                cancellationToken) +
        await db.ExamPublishJobs.AsNoTracking()
            .CountAsync(
            x => x.Status == ExamPublishJobStatus.Failed &&
            x.CreatedAt >= from,
            cancellationToken);

    private async Task<bool> IsSwaggerEnabledAsync(CancellationToken cancellationToken) =>
        await db.SystemFeatureSettings.AsNoTracking()
            .Where(x => x.Key == SystemFeatureKeys.SwaggerDocumentation)
            .Select(x => (bool?)x.IsEnabled)
            .SingleOrDefaultAsync(cancellationToken) ?? false;

    private ActionResult? ValidatePaging(int page, int pageSize)
    {
        if (page is < 1 or > 100000 || pageSize is < 1 or > 100)
        {
            return ValidationProblem(
                "页码必须在 1 到 100000 之间，每页数量必须在 1 到 100 之间。");
        }
        return null;
    }

    private ActionResult? ValidateRange(DateTime? from, DateTime? to)
    {
        if (from.HasValue && to.HasValue && from.Value > to.Value)
            return ValidationProblem("开始时间不能晚于结束时间。");
        return null;
    }

    private static string? NormalizeJobKind(string? kind)
    {
        if (string.IsNullOrWhiteSpace(kind)) return null;
        return kind.Trim() switch
        {
            "AutomaticSchedule" => "AutomaticSchedule",
            "SchedulePublish" => "SchedulePublish",
            "MakeupExamAuto" => "MakeupExamAuto",
            "ExamArrangement" => "ExamArrangement",
            _ => null
        };
    }

    private static string SafeMessage(Exception exception)
    {
        var message = exception.GetBaseException().Message;
        return message.Length <= 500 ? message : message[..500];
    }
}

public sealed record AuditLogItem(
    Guid Id,
    string? UserName,
    string Method,
    string Path,
    int StatusCode,
    string? IpAddress,
    DateTime CreatedAt);

public sealed record FailedBackgroundJobItem(
    Guid Id,
    string Kind,
    string KindLabel,
    string Context,
    string ErrorMessage,
    DateTime CreatedAt,
    DateTime? StartedAt,
    DateTime? CompletedAt,
    int ProcessingAttempts);

public sealed record OperationalAlert(
    string Id,
    string Severity,
    string Source,
    string Title,
    string Detail,
    DateTime OccurredAt);

public sealed record OperationsCounters(
    int AuditEvents24Hours,
    int ServerErrors24Hours,
    int FailedJobs7Days,
    int CriticalAlerts);

public sealed record OperationsSummary(
    DateTime GeneratedAt,
    OperationalHealthSnapshot Health,
    OperationsCounters Counters,
    IReadOnlyCollection<OperationalAlert> Alerts,
    BackupArtifact? LatestBackup);

public sealed record CreateBackupRequest([MaxLength(200)] string? Note);

public sealed record RestoreDrillRequest([Required] string Confirmation);

public sealed record SwaggerDocumentationSettings(bool IsEnabled);

public sealed record UpdateSwaggerDocumentationSettings(bool IsEnabled);
