using System.ComponentModel.DataAnnotations;
using Jiaowu.Api.Contracts;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.OfficialDocuments;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace Jiaowu.Api.Controllers;

[ApiController]
[Route("api/official-documents")]
public sealed class OfficialDocumentsController(
    AppDbContext db,
    ICurrentUserDataScope currentUserDataScope,
    OfficialDocumentService service,
    IOptions<OfficialDocumentOptions> options) : ControllerBase
{
    private const string Managers =
        SystemRoles.SuperAdmin + "," +
        SystemRoles.AcademicAdmin + "," +
        SystemRoles.CollegeAdmin;
    private const string Readers = Managers + "," + SystemRoles.Student;

    [HttpGet]
    [Authorize(Roles = Readers)]
    public async Task<ActionResult> List(
        OfficialDocumentType? type,
        OfficialDocumentStatus? status,
        Guid? studentId,
        CancellationToken cancellationToken,
        int page = 1,
        int pageSize = 20)
    {
        if (page < 1 || pageSize is < 1 or > 100)
            return ValidationProblem("页码必须大于 0，且每页条数应在 1 至 100 之间。");

        var source = AccessibleDocuments().AsNoTracking();
        if (type.HasValue) source = source.Where(x => x.Type == type);
        if (status.HasValue) source = source.Where(x => x.Status == status);
        if (studentId.HasValue) source = source.Where(x => x.StudentId == studentId);

        var total = await source.CountAsync(cancellationToken);
        var items = await source
            .OrderByDescending(x => x.IssuedAt)
            .ThenByDescending(x => x.Id)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(x => new
            {
                x.Id,
                x.DocumentNumber,
                x.Type,
                x.Status,
                x.StudentId,
                x.Student!.StudentNumber,
                StudentName = x.Student.Name,
                CollegeName = x.Student.AdministrativeClass!.Major!.College!.Name,
                x.Purpose,
                x.IssuedAt,
                IssuedByName = x.Student.UserId == x.IssuedByUserId
                    ? "学生自助服务"
                    : x.IssuedByUser!.DisplayName,
                x.InvalidatedAt,
                x.InvalidationReason,
                x.ReissuedFromDocumentId,
                DownloadCount = x.Downloads.Count,
                LastDownloadedAt = x.Downloads.Max(download => (DateTime?)download.CreatedAt)
            })
            .ToListAsync(cancellationToken);
        return Ok(new PagedResult<object>(items, total, page, pageSize));
    }

    [HttpGet("students/options")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> StudentOptions(
        string? keyword,
        CancellationToken cancellationToken,
        int page = 1,
        int pageSize = 30)
    {
        if (page < 1 || pageSize is < 1 or > 100)
            return ValidationProblem("页码必须大于 0，且每页条数应在 1 至 100 之间。");

        var source = AccessibleStudents().AsNoTracking();
        if (!string.IsNullOrWhiteSpace(keyword))
        {
            var value = keyword.Trim();
            source = source.Where(x =>
                x.StudentNumber.Contains(value) || x.Name.Contains(value));
        }

        var total = await source.CountAsync(cancellationToken);
        var items = await source.OrderBy(x => x.StudentNumber)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(x => new
            {
                x.Id,
                x.StudentNumber,
                x.Name,
                x.Status,
                ClassName = x.AdministrativeClass!.Name,
                MajorName = x.AdministrativeClass.Major!.Name,
                CollegeName = x.AdministrativeClass.Major.College!.Name
            })
            .ToListAsync(cancellationToken);
        return Ok(new PagedResult<object>(items, total, page, pageSize));
    }

    [HttpPost]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> Issue(
        IssueOfficialDocumentRequest request,
        CancellationToken cancellationToken)
    {
        if (!await AccessibleStudents().AnyAsync(x => x.Id == request.StudentId, cancellationToken))
            return NotFound();

        try
        {
            var document = await service.CreateAsync(
                request.StudentId,
                request.Type,
                request.Purpose,
                currentUserDataScope.Current.UserId,
                currentUserDataScope.Current.DisplayName ?? "教务管理员",
                VerificationBaseUrl(),
                null,
                cancellationToken);
            db.OfficialDocuments.Add(document);
            await db.SaveChangesAsync(cancellationToken);
            return CreatedAtAction(nameof(Download), new { id = document.Id },
                new { document.Id, document.DocumentNumber });
        }
        catch (OfficialDocumentValidationException exception)
        {
            return ConflictProblem(exception.Message);
        }
    }

    [HttpPost("mine")]
    [Authorize(Roles = SystemRoles.Student)]
    public async Task<ActionResult> IssueMine(
        SelfIssueOfficialDocumentRequest request,
        CancellationToken cancellationToken)
    {
        var userId = currentUserDataScope.Current.UserId;
        var studentId = await db.Students.AsNoTracking()
            .Where(x => x.UserId == userId)
            .Select(x => (Guid?)x.Id)
            .SingleOrDefaultAsync(cancellationToken);
        if (!studentId.HasValue)
            return ConflictProblem("当前账号未关联有效学生档案，不能申请电子凭证。");

        var purpose = string.IsNullOrWhiteSpace(request.Purpose)
            ? null
            : request.Purpose.Trim();
        var duplicateThreshold = DateTime.UtcNow.AddMinutes(-5);
        var recentDocument = await db.OfficialDocuments.AsNoTracking()
            .Where(x =>
                x.StudentId == studentId.Value &&
                x.Type == request.Type &&
                x.Status == OfficialDocumentStatus.Valid &&
                x.Purpose == purpose &&
                x.IssuedAt >= duplicateThreshold)
            .OrderByDescending(x => x.IssuedAt)
            .Select(x => new { x.Id, x.DocumentNumber })
            .FirstOrDefaultAsync(cancellationToken);
        if (recentDocument is not null)
            return Ok(new
            {
                recentDocument.Id,
                recentDocument.DocumentNumber,
                Reused = true
            });

        try
        {
            var document = await service.CreateAsync(
                studentId.Value,
                request.Type,
                purpose,
                userId,
                $"{options.Value.IssuingOffice}自助签发",
                VerificationBaseUrl(),
                null,
                cancellationToken);
            db.OfficialDocuments.Add(document);
            await db.SaveChangesAsync(cancellationToken);
            return CreatedAtAction(nameof(Download), new { id = document.Id },
                new { document.Id, document.DocumentNumber, Reused = false });
        }
        catch (OfficialDocumentValidationException exception)
        {
            return ConflictProblem(exception.Message);
        }
    }

    [HttpGet("{id:guid}/download")]
    [Authorize(Roles = Readers)]
    public async Task<IActionResult> Download(Guid id, CancellationToken cancellationToken)
    {
        var document = await AccessibleDocuments()
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
        if (document is null) return NotFound();
        if (document.Status != OfficialDocumentStatus.Valid)
            return ConflictProblem("该凭证已经失效或被新凭证替代，不能继续下载。");

        db.OfficialDocumentDownloads.Add(new OfficialDocumentDownload
        {
            OfficialDocumentId = document.Id,
            DownloadedByUserId = currentUserDataScope.Current.UserId,
            IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
            UserAgent = Request.Headers.UserAgent.ToString()
        });
        await db.SaveChangesAsync(cancellationToken);
        return File(document.PdfContent, "application/pdf", $"{document.DocumentNumber}.pdf");
    }

    [HttpGet("{id:guid}/downloads")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> DownloadHistory(
        Guid id,
        CancellationToken cancellationToken,
        int page = 1,
        int pageSize = 20)
    {
        if (page < 1 || pageSize is < 1 or > 100)
            return ValidationProblem("页码必须大于 0，且每页条数应在 1 至 100 之间。");

        if (!await AccessibleDocuments().AnyAsync(x => x.Id == id, cancellationToken))
            return NotFound();

        var source = db.OfficialDocumentDownloads.AsNoTracking()
            .Where(x => x.OfficialDocumentId == id)
            .OrderByDescending(x => x.CreatedAt)
            .ThenByDescending(x => x.Id);
        var total = await source.CountAsync(cancellationToken);
        var items = await source
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(x => new
            {
                x.Id,
                x.CreatedAt,
                x.DownloadedByUserId,
                DownloadedByName = x.DownloadedByUser!.DisplayName,
                x.IpAddress,
                x.UserAgent
            })
            .ToListAsync(cancellationToken);
        return Ok(new PagedResult<object>(items, total, page, pageSize));
    }

    [HttpPost("{id:guid}/invalidate")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> Invalidate(
        Guid id,
        InvalidateOfficialDocumentRequest request,
        CancellationToken cancellationToken)
    {
        var document = await AccessibleDocuments()
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
        if (document is null) return NotFound();
        if (document.Status != OfficialDocumentStatus.Valid)
            return ConflictProblem("该凭证当前已不是有效状态。");

        document.Status = OfficialDocumentStatus.Invalidated;
        document.InvalidatedAt = DateTime.UtcNow;
        document.InvalidatedByUserId = currentUserDataScope.Current.UserId;
        document.InvalidationReason = request.Reason.Trim();
        await db.SaveChangesAsync(cancellationToken);
        return Ok(new { document.Id, document.Status, document.InvalidatedAt });
    }

    [HttpPost("{id:guid}/reissue")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> Reissue(
        Guid id,
        ReissueOfficialDocumentRequest request,
        CancellationToken cancellationToken)
    {
        try
        {
            var newDocument = await db.ExecuteInRetriableTransactionAsync(async transaction =>
            {
                var original = await AccessibleDocuments()
                    .FirstOrDefaultAsync(x => x.Id == id, cancellationToken)
                    ?? throw new OfficialDocumentNotFoundException();
                if (await db.OfficialDocuments.AnyAsync(
                        x => x.ReissuedFromDocumentId == original.Id,
                        cancellationToken))
                    throw new OfficialDocumentValidationException("该凭证已经重签过，不能重复重签。");

                var replacement = await service.CreateAsync(
                    original.StudentId,
                    original.Type,
                    request.Purpose ?? original.Purpose,
                    currentUserDataScope.Current.UserId,
                    currentUserDataScope.Current.DisplayName ?? "教务管理员",
                    VerificationBaseUrl(),
                    original.Id,
                    cancellationToken);
                original.Status = OfficialDocumentStatus.Superseded;
                original.InvalidatedAt = DateTime.UtcNow;
                original.InvalidatedByUserId = currentUserDataScope.Current.UserId;
                original.InvalidationReason = request.Reason.Trim();
                db.OfficialDocuments.Add(replacement);
                await db.SaveChangesAsync(cancellationToken);
                await transaction.CommitAsync(cancellationToken);
                return replacement;
            }, cancellationToken);

            return Ok(new { newDocument.Id, newDocument.DocumentNumber });
        }
        catch (OfficialDocumentNotFoundException)
        {
            return NotFound();
        }
        catch (OfficialDocumentValidationException exception)
        {
            return ConflictProblem(exception.Message);
        }
    }

    [HttpGet("verify/{verificationCode}")]
    [AllowAnonymous]
    [EnableRateLimiting("official-verification")]
    public async Task<ActionResult> Verify(
        string verificationCode,
        CancellationToken cancellationToken)
    {
        var normalized = verificationCode.Trim();
        if (normalized.Length is < 12 or > 128)
            return Ok(new { Found = false, Valid = false });

        var hash = OfficialDocumentService.HashVerificationCode(normalized);
        var documentNumber = normalized.ToUpperInvariant();
        var document = await db.OfficialDocuments.AsNoTracking()
            .FirstOrDefaultAsync(x =>
                x.VerificationCodeHash == hash ||
                x.DocumentNumber == documentNumber,
                cancellationToken);
        if (document is null) return Ok(new { Found = false, Valid = false });

        var snapshot = OfficialDocumentService.DeserializeSnapshot(document.SnapshotJson);
        var replacementNumber = await db.OfficialDocuments.AsNoTracking()
            .Where(x => x.ReissuedFromDocumentId == document.Id)
            .Select(x => x.DocumentNumber)
            .FirstOrDefaultAsync(cancellationToken);
        return Ok(new
        {
            Found = true,
            Valid = document.Status == OfficialDocumentStatus.Valid,
            document.Status,
            document.DocumentNumber,
            document.Type,
            snapshot.InstitutionName,
            StudentName = MaskName(snapshot.Student.Name),
            StudentNumber = MaskStudentNumber(snapshot.Student.StudentNumber),
            snapshot.Student.CollegeName,
            snapshot.Student.MajorName,
            document.IssuedAt,
            document.InvalidatedAt,
            ReplacementDocumentNumber = replacementNumber,
            PdfSha256 = document.PdfSha256
        });
    }

    private IQueryable<OfficialDocument> AccessibleDocuments()
    {
        var scope = currentUserDataScope.Current;
        var source = db.OfficialDocuments.AsQueryable();
        var isManager = scope.IsInRole(SystemRoles.SuperAdmin) ||
                        scope.IsInRole(SystemRoles.AcademicAdmin) ||
                        scope.IsInRole(SystemRoles.CollegeAdmin);
        if (scope.IsInRole(SystemRoles.Student) && !isManager)
            return source.Where(x => x.Student!.UserId == scope.UserId);
        if (scope.RestrictedCollegeId.HasValue)
            return source.Where(x =>
                x.Student!.AdministrativeClass!.Major!.CollegeId ==
                scope.RestrictedCollegeId.Value);
        return source;
    }

    private IQueryable<Student> AccessibleStudents()
    {
        var source = db.Students.AsQueryable();
        var collegeId = currentUserDataScope.Current.RestrictedCollegeId;
        return collegeId.HasValue
            ? source.Where(x => x.AdministrativeClass!.Major!.CollegeId == collegeId.Value)
            : source;
    }

    private string VerificationBaseUrl()
    {
        var configured = options.Value.PublicBaseUrl;
        if (!string.IsNullOrWhiteSpace(configured)) return configured.TrimEnd('/');
        return $"{Request.Scheme}://{Request.Host}{Request.PathBase}";
    }

    private static string MaskName(string name) => name.Length switch
    {
        0 => "*",
        1 => name,
        2 => name[0] + "*",
        _ => name[0] + new string('*', name.Length - 2) + name[^1]
    };

    private static string MaskStudentNumber(string number) => number.Length <= 4
        ? new string('*', number.Length)
        : number[..2] + new string('*', number.Length - 4) + number[^2..];

    private ConflictObjectResult ConflictProblem(string detail) => Conflict(new ProblemDetails
    {
        Title = "当前状态不允许此操作",
        Detail = detail,
        Status = StatusCodes.Status409Conflict
    });
}

public sealed record IssueOfficialDocumentRequest(
    Guid StudentId,
    OfficialDocumentType Type,
    [param: StringLength(200)] string? Purpose);

public sealed record SelfIssueOfficialDocumentRequest(
    OfficialDocumentType Type,
    [param: StringLength(200)] string? Purpose);

public sealed record InvalidateOfficialDocumentRequest(
    [param: Required, StringLength(500, MinimumLength = 2)] string Reason);

public sealed record ReissueOfficialDocumentRequest(
    [param: Required, StringLength(500, MinimumLength = 2)] string Reason,
    [param: StringLength(200)] string? Purpose);

file sealed class OfficialDocumentNotFoundException : Exception;
