using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Jiaowu.Api.Contracts;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Excel;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Controllers;

[ApiController]
[Authorize]
[Route("api/other-exams")]
public sealed class OtherExamsController(AppDbContext db, ICurrentUserDataScope scope) : ControllerBase
{
    private const string Managers = SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin;

    [HttpGet("batches")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> GetBatches(
        string? keyword = null,
        OtherExamBatchStatus? status = null,
        OtherExamMetricKind? metricKind = null,
        DateOnly? examDateFrom = null,
        DateOnly? examDateTo = null,
        int page = 1,
        int pageSize = 20,
        CancellationToken ct = default)
    {
        page = Math.Max(1, page);
        pageSize = Math.Clamp(pageSize, 10, 100);
        var source = db.OtherExamBatches.AsNoTracking();
        if (!string.IsNullOrWhiteSpace(keyword))
        {
            keyword = keyword.Trim();
            source = source.Where(x =>
                (x.ExamCode != null && x.ExamCode.Contains(keyword)) ||
                x.Name.Contains(keyword) ||
                (x.Organizer != null && x.Organizer.Contains(keyword)));
        }
        if (status.HasValue) source = source.Where(x => x.Status == status.Value);
        if (metricKind.HasValue) source = source.Where(x => x.MetricKind == metricKind.Value);
        if (examDateFrom.HasValue) source = source.Where(x => x.ExamDate >= examDateFrom.Value);
        if (examDateTo.HasValue) source = source.Where(x => x.ExamDate <= examDateTo.Value);
        var total = await source.CountAsync(ct);
        var items = await source.OrderByDescending(x => x.ExamDate).ThenByDescending(x => x.CreatedAt)
        .Skip((page - 1) * pageSize).Take(pageSize)
        .Select(x => new { x.Id, x.ExamCode, x.Name, x.Organizer, x.ExamDate, x.MetricKind, x.MaxScore, x.LevelOptions, x.Status, x.PublicationCount, x.PublishedAt, ResultCount = x.Results.Count })
        .ToListAsync(ct);
        return Ok(new PagedResult<object>(items, total, page, pageSize));
    }

    [HttpPost("batches")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> CreateBatch(CreateOtherExamRequest request, CancellationToken ct)
    {
        var code = Normalize(request.ExamCode)?.ToUpperInvariant();
        var name = Normalize(request.Name);
        if (code is null || name is null) return ValidationProblem("考试编码和考试名称不能为空。");
        var error = ValidateDefinition(request.MetricKind, request.MaxScore, request.LevelOptions);
        if (error is not null) return ValidationProblem(error);
        var definitionConflict = await db.OtherExamBatches.AnyAsync(x =>
            x.ExamCode == code && (x.MetricKind != request.MetricKind || x.MaxScore != request.MaxScore || x.LevelOptions != Normalize(request.LevelOptions)), ct);
        if (definitionConflict) return ConflictProblem("同一考试编码已经使用了不同的评价方式或评价参数，请检查考试编码。");
        var batch = new OtherExamBatch { ExamCode = code, Name = name, Organizer = Normalize(request.Organizer), ExamDate = request.ExamDate, MetricKind = request.MetricKind, MaxScore = request.MaxScore, LevelOptions = Normalize(request.LevelOptions) };
        db.OtherExamBatches.Add(batch);
        await db.SaveChangesAsync(ct);
        return Ok(new { batch.Id });
    }

    [HttpGet("students/lookup")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> LookupStudent(string studentNumber, CancellationToken ct)
    {
        var number = Normalize(studentNumber);
        if (number is null) return ValidationProblem("请输入学号。");
        var student = await db.Students.AsNoTracking().Where(x => x.StudentNumber == number)
            .Select(x => new { x.Id, x.StudentNumber, x.Name, CollegeName = x.AdministrativeClass!.Major!.College!.Name, ClassName = x.AdministrativeClass!.Name }).FirstOrDefaultAsync(ct);
        return student is null ? NotFound(new ProblemDetails { Detail = "未找到该学号对应的学生档案。", Status = 404 }) : Ok(student);
    }

    [HttpGet("batches/{id:guid}")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> GetBatch(Guid id, CancellationToken ct)
    {
        var batch = await db.OtherExamBatches.AsNoTracking().Where(x => x.Id == id)
            .Select(x => new { x.Id, x.ExamCode, x.Name, x.Organizer, x.ExamDate, x.MetricKind, x.MaxScore, x.LevelOptions, x.Status, x.PublicationCount, x.PublishedAt }).FirstOrDefaultAsync(ct);
        if (batch is null) return NotFound();
        var results = await db.OtherExamResults.AsNoTracking().Where(x => x.OtherExamBatchId == id)
            .OrderBy(x => x.Student!.StudentNumber)
            .Select(x => new { x.Id, x.StudentId, StudentNumber = x.Student!.StudentNumber, StudentName = x.Student.Name, CollegeName = x.Student.AdministrativeClass!.Major!.College!.Name, ClassName = x.Student.AdministrativeClass.Name, x.AttemptNumber, x.Score, x.Level, x.IsPassed, x.Notes }).ToListAsync(ct);
        return Ok(new { Batch = batch, Results = results });
    }

    [HttpPut("batches/{id:guid}/results")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> ReplaceResults(Guid id, ReplaceOtherExamResultsRequest request, CancellationToken ct)
    {
        var batch = await db.OtherExamBatches.Include(x => x.Results).FirstOrDefaultAsync(x => x.Id == id, ct);
        if (batch is null) return NotFound();
        var result = await ReplaceResultsAsync(batch, request.Results, ct);
        return result is null ? Ok(new { updated = request.Results.Count }) : result;
    }

    [HttpGet("batches/{id:guid}/template")]
    [Authorize(Roles = Managers)]
    public async Task<IActionResult> DownloadTemplate(Guid id, CancellationToken ct)
    {
        var batch = await db.OtherExamBatches.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
        if (batch is null) return NotFound();
        var headers = HeadersFor(batch);
        var bytes = ExcelWorkbookHelper.Create("其他考试成绩导入", headers, [], ["第一行为表头，请勿修改；每行填写一名学生。", "学号用于自动匹配姓名、学院和班级，参加次数由系统自动计算。"]);
        return File(bytes, ExcelWorkbookHelper.ContentType, $"其他考试成绩导入模板-{batch.ExamCode ?? batch.Name}.xlsx");
    }

    [HttpPost("batches/{id:guid}/import")]
    [Authorize(Roles = Managers)]
    [RequestSizeLimit(10 * 1024 * 1024)]
    public async Task<ActionResult> Import(Guid id, IFormFile file, CancellationToken ct)
    {
        var batch = await db.OtherExamBatches.Include(x => x.Results).FirstOrDefaultAsync(x => x.Id == id, ct);
        if (batch is null) return NotFound();
        IReadOnlyList<ExcelRow> rows;
        try { rows = await ExcelWorkbookHelper.ReadAsync(file, HeadersFor(batch), ct); }
        catch (InvalidDataException ex) { return ValidationProblem(ex.Message); }
        if (rows.Count == 0) return ValidationProblem("Excel 中没有可导入的成绩数据。");
        var inputs = new List<OtherExamResultRequest>();
        var errors = new List<string>();
        foreach (var row in rows)
        {
            var number = row["学号"].Trim();
            if (number.Length == 0) { errors.Add($"第 {row.RowNumber} 行：学号不能为空。"); continue; }
            var score = batch.MetricKind == OtherExamMetricKind.Score ? ParseScore(row, batch, errors) : null;
            var level = batch.MetricKind == OtherExamMetricKind.Level ? Normalize(row["等级"]) : null;
            var passed = batch.MetricKind == OtherExamMetricKind.PassFail ? ParsePass(row["是否合格"], row.RowNumber, errors) : null;
            inputs.Add(new OtherExamResultRequest(number, score, level, passed, Normalize(row["备注"])));
        }
        if (errors.Count > 0) return ImportValidationProblem(errors);
        var result = await ReplaceResultsAsync(batch, inputs, ct);
        return result ?? Ok(new { updated = inputs.Count });
    }

    [HttpPost("batches/{id:guid}/publish")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> Publish(Guid id, CancellationToken ct)
    {
        var batch = await db.OtherExamBatches.Include(x => x.Results).FirstOrDefaultAsync(x => x.Id == id, ct);
        if (batch is null) return NotFound();
        if (batch.Results.Count == 0) return ConflictProblem("没有成绩记录，不能发布。");
        batch.Status = OtherExamBatchStatus.Published;
        batch.PublicationCount++;
        batch.PublishedAt = DateTime.UtcNow;
        await db.SaveChangesAsync(ct);
        return Ok(new { batch.PublicationCount, batch.PublishedAt });
    }

    [HttpGet("mine")]
    [Authorize(Roles = SystemRoles.Student)]
    public async Task<ActionResult> Mine(CancellationToken ct)
    {
        var studentId = await db.Students.Where(x => x.UserId == scope.Current.UserId).Select(x => (Guid?)x.Id).FirstOrDefaultAsync(ct);
        if (studentId is null) return ConflictProblem("当前账号未关联有效学生档案。");
        var history = await db.OtherExamResults.AsNoTracking().Where(x => x.StudentId == studentId && x.OtherExamBatch!.Status == OtherExamBatchStatus.Published)
            .OrderByDescending(x => x.OtherExamBatch!.ExamDate).ThenByDescending(x => x.AttemptNumber)
            .Select(x => new { x.Id, ExamCode = x.OtherExamBatch!.ExamCode ?? x.OtherExamBatch.Name, BatchId = x.OtherExamBatchId, ExamName = x.OtherExamBatch.Name, x.OtherExamBatch.ExamDate, x.OtherExamBatch.MetricKind, x.OtherExamBatch.MaxScore, x.OtherExamBatch.LevelOptions, x.AttemptNumber, x.Score, x.Level, x.IsPassed, x.OtherExamBatch.PublishedAt }).ToListAsync(ct);
        var best = history.GroupBy(x => x.ExamCode).Select(g => g.OrderByDescending(x => Rank(x.MetricKind, x.Score, x.Level, x.IsPassed, x.LevelOptions)).ThenByDescending(x => x.ExamDate).First()).ToList();
        return Ok(new { Best = best, History = history });
    }

    private async Task<ActionResult?> ReplaceResultsAsync(OtherExamBatch batch, IReadOnlyList<OtherExamResultRequest> inputs, CancellationToken ct)
    {
        var numbers = inputs.Select(x => x.StudentNumber.Trim()).ToList();
        if (numbers.Count != numbers.Distinct(StringComparer.OrdinalIgnoreCase).Count()) return ValidationProblem("同一考试批次中学生不能重复出现。");
        var studentRows = await db.Students
            .Where(x => numbers.Contains(x.StudentNumber))
            .ToListAsync(ct);
        var students = studentRows.ToDictionary(x => x.StudentNumber, StringComparer.OrdinalIgnoreCase);
        if (students.Count != numbers.Count) return ValidationProblem("存在不存在的学号，请先检查学生档案。");
        foreach (var item in inputs)
        {
            var error = ValidateResult(batch, item.Score, item.Level, item.IsPassed, item.Notes);
            if (error is not null) return ValidationProblem(error);
        }
        var studentIds = students.Values.Select(x => x.Id).ToList();
        var beforeCount = await db.OtherExamResults.AsNoTracking()
            .Where(x => x.OtherExamBatchId != batch.Id && studentIds.Contains(x.StudentId) && (x.OtherExamBatch!.ExamCode == batch.ExamCode || (x.OtherExamBatch.ExamCode == null && batch.ExamCode == null && x.OtherExamBatch.Name == batch.Name)) && (x.OtherExamBatch.ExamDate < batch.ExamDate || (x.OtherExamBatch.ExamDate == batch.ExamDate && x.OtherExamBatch.CreatedAt < batch.CreatedAt)))
            .GroupBy(x => x.StudentId).Select(x => new { StudentId = x.Key, Count = x.Count() }).ToDictionaryAsync(x => x.StudentId, x => x.Count, ct);
        return await db.ExecuteInRetriableTransactionAsync<ActionResult?>(async transaction =>
        {
            db.OtherExamResults.RemoveRange(batch.Results);
            batch.Status = OtherExamBatchStatus.Draft;
            await db.SaveChangesAsync(ct);

            var replacementResults = inputs.Select(x =>
            {
                var student = students[x.StudentNumber.Trim()];
                return new OtherExamResult
                {
                    OtherExamBatchId = batch.Id,
                    StudentId = student.Id,
                    AttemptNumber = beforeCount.GetValueOrDefault(student.Id) + 1,
                    Score = x.Score,
                    Level = Normalize(x.Level),
                    IsPassed = x.IsPassed,
                    Notes = Normalize(x.Notes)
                };
            }).ToList();
            db.OtherExamResults.AddRange(replacementResults);
            await db.SaveChangesAsync(ct);
            await transaction.CommitAsync(ct);
            return null;
        }, ct);
    }

    private static string[] HeadersFor(OtherExamBatch batch) => batch.MetricKind switch
    {
        OtherExamMetricKind.Score => ["学号", "成绩", "备注"],
        OtherExamMetricKind.Level => ["学号", "等级", "备注"],
        _ => ["学号", "是否合格", "备注"]
    };
    private static decimal? ParseScore(ExcelRow row, OtherExamBatch batch, List<string> errors)
    {
        if (decimal.TryParse(row["成绩"], NumberStyles.Number, CultureInfo.InvariantCulture, out var value) && value >= 0 && value <= batch.MaxScore) return value;
        errors.Add($"第 {row.RowNumber} 行：成绩必须在 0 到 {batch.MaxScore:0.##} 之间。"); return null;
    }
    private static bool? ParsePass(string value, int row, List<string> errors)
    {
        if (value is "合格" or "是" or "通过" or "true" or "True") return true;
        if (value is "不合格" or "否" or "未通过" or "false" or "False") return false;
        errors.Add($"第 {row} 行：是否合格请填写合格或不合格。"); return null;
    }
    private static string? ValidateDefinition(OtherExamMetricKind kind, decimal? max, string? levels) => kind switch
    {
        OtherExamMetricKind.Score when !max.HasValue || max <= 0 => "分数制必须填写大于 0 的满分。",
        OtherExamMetricKind.Level when string.IsNullOrWhiteSpace(levels) => "等级制必须填写等级选项。",
        _ => null
    };
    private static string? ValidateResult(OtherExamBatch b, decimal? score, string? level, bool? pass, string? notes)
    {
        if (Normalize(notes)?.Length > 500) return "备注不能超过 500 个字符。";
        return b.MetricKind switch
        {
            OtherExamMetricKind.Score when !score.HasValue || score < 0 || score > b.MaxScore => "分数必须在 0 到满分之间。",
            OtherExamMetricKind.Score when decimal.Round(score.Value, 2) != score.Value => "分数最多保留两位小数。",
            OtherExamMetricKind.Level when string.IsNullOrWhiteSpace(level) => "等级制必须填写等级。",
            OtherExamMetricKind.Level when Normalize(level)!.Length > 50 => "等级不能超过 50 个字符。",
            OtherExamMetricKind.Level when !(b.LevelOptions ?? "").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Contains(Normalize(level)!, StringComparer.Ordinal) => "等级必须从该考试场次配置的等级选项中选择。",
            OtherExamMetricKind.PassFail when !pass.HasValue => "合格/不合格考试必须填写结果。",
            _ => null
        };
    }
    private static int Rank(OtherExamMetricKind kind, decimal? score, string? level, bool? pass, string? options)
    {
        if (kind == OtherExamMetricKind.Score) return (int)((score ?? -1) * 1000);
        if (kind == OtherExamMetricKind.PassFail) return pass == true ? 1 : 0;
        var levels = (options ?? "").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
        var index = Array.IndexOf(levels, level ?? "");
        return index >= 0 ? levels.Length - index : -1;
    }
    private ActionResult ImportValidationProblem(IReadOnlyList<string> errors)
    {
        foreach (var error in errors.Take(50)) ModelState.AddModelError("file", error);
        return ValidationProblem(ModelState);
    }
    private static string? Normalize(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
    private static ConflictObjectResult ConflictProblem(string message) => new(new ProblemDetails { Status = 409, Detail = message });
}

public sealed record CreateOtherExamRequest([Required] string ExamCode, [Required] string Name, DateOnly ExamDate, OtherExamMetricKind MetricKind, decimal? MaxScore, string? LevelOptions, string? Organizer);
public sealed record ReplaceOtherExamResultsRequest(List<OtherExamResultRequest> Results);
public sealed record OtherExamResultRequest([Required] string StudentNumber, decimal? Score, string? Level, bool? IsPassed, string? Notes);
