using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;


/// <summary>
/// Batch CRUD, document upload, and assignment for the digitization workflow.
/// </summary>
[ApiController]
[Route("api/v1/digitization-batches")]
[Produces("application/json")]
[Authorize]
public class DigitizationBatchesController : ControllerBase
{
    private readonly IBatchService _batches;
    private readonly IDocumentStorageService _storage;
    private readonly IPromotionService _promotion;
    private readonly IBatchEventService _batchEventService;
    private readonly ICoverSheetService _coverSheets;
    private readonly AppDbContext _db;

    private static readonly HashSet<string> _allowedMimeTypes = new()
    {
        "application/pdf", "image/jpeg", "image/png"
    };

    public DigitizationBatchesController(
        IBatchService batches,
        IDocumentStorageService storage,
        IPromotionService promotion,
        IBatchEventService batchEventService,
        AppDbContext db,
        ICoverSheetService coverSheets)
    {
        _batches = batches;
        _storage = storage;
        _promotion = promotion;
        _batchEventService = batchEventService;
        _db = db;
        _coverSheets = coverSheets;
    }

    /// <summary>
    /// Uploads a scanned document and creates a new digitization batch.
    /// When supersedesBatchId is provided, the batch is treated as a correction
    /// that will supersede the erroneous promoted batch upon its own promotion.
    /// </summary>
    [HttpPost]
    [ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status422UnprocessableEntity)]
    [RequestSizeLimit(25 * 1024 * 1024)]
    [Consumes("multipart/form-data")]
    public async Task<IActionResult> Create([FromForm] CreateBatchForm form)
    {
        if (form.File is null || form.File.Length == 0)
            return BadRequest(ApiResponse<object>.Fail(400, "File is required.", "EMPTY_FILE"));

        if (!_allowedMimeTypes.Contains(form.File.ContentType))
            return BadRequest(ApiResponse<object>.Fail(400,
                "Accepted formats: PDF, JPEG, PNG.", "INVALID_MIME_TYPE"));

        CoverSheet? coverSheet = null;
        BatchType parsedBatchType;
        BatchTrack parsedTrack;

        if (!string.IsNullOrWhiteSpace(form.CoverSheetCode))
        {
            coverSheet = await _coverSheets.LookupByCodeAsync(form.CoverSheetCode);
            if (coverSheet is null)
                return NotFound(ApiResponse<object>.Fail(404,
                    "Cover sheet not found.", "COVER_SHEET_NOT_FOUND"));

            if (coverSheet.IsUsed)
                return Conflict(ApiResponse<object>.Fail(409,
                    $"Cover sheet {coverSheet.Code} has already been used.",
                    "COVER_SHEET_ALREADY_USED"));

            parsedBatchType = coverSheet.BatchType;
            parsedTrack = coverSheet.Track;
            if (coverSheet.PatientId.HasValue)
                form.PatientId ??= coverSheet.PatientId;
        }
        else
        {
            if (string.IsNullOrWhiteSpace(form.BatchType))
                return BadRequest(ApiResponse<object>.Fail(400,
                    "BatchType is required when no cover sheet code is provided.",
                    "MISSING_BATCH_TYPE"));

            parsedBatchType = BatchTypeExtensions.FromDbString(form.BatchType.ToUpperInvariant());
            parsedTrack = string.IsNullOrEmpty(form.Track)
                ? BatchTrack.Backfill
                : BatchTrackExtensions.FromDbString(form.Track.ToUpperInvariant());
        }

        var actorUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);

        using var stream = form.File.OpenReadStream();
        var result = await _batches.CreateAsync(
            stream, form.File.ContentType, parsedBatchType, parsedTrack,
            form.PatientId, form.SupersedesBatchId, actorUserId);

        var batch = result.Batch;
        if (coverSheet is not null)
        {
            await _coverSheets.RedeemAsync(coverSheet.Id, batch.Id);

            if (coverSheet.AssignToUserId.HasValue)
                batch = await _batches.AssignAsync(batch.Id, coverSheet.AssignToUserId.Value, actorUserId);
        }

        return StatusCode(201, ApiResponse<BatchDetailResponse>.Created(
            BatchDetailResponse.FromEntity(batch, supersession: result.Supersession)));
    }

    /// <summary>
    /// Gets a batch by ID with a presigned document URL (15-minute expiry).
    /// </summary>
    [HttpGet("{id:guid}")]
    [ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Get(Guid id)
    {
        var batch = await _batches.GetByIdAsync(id);
        var presignedUrl = await _storage.GetPresignedUrlAsync(batch.DocumentRef);

        await RecordDocumentAccessAsync(id);

        return Ok(ApiResponse<BatchDetailResponse>.Ok(
            BatchDetailResponse.FromEntity(batch, presignedUrl)));
    }

    /// <summary>
    /// Streams the scanned document for in-app viewing (same auth and audit as GET batch).
    /// Proxied through the API so the workstation can render PDFs without cross-origin iframe issues.
    /// </summary>
    [HttpGet("{id:guid}/document")]
    [Produces("application/pdf", "image/jpeg", "image/png", "application/octet-stream")]
    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetDocument(Guid id)
    {
        var batch = await _batches.GetByIdAsync(id);

        if (batch.DocumentRef == "live-capture")
            return NotFound(ApiResponse<object>.Fail(
                404, "This batch has no scanned document.", "NO_DOCUMENT"));

        var scanned = await _db.ScannedDocuments
            .AsNoTracking()
            .FirstOrDefaultAsync(d => d.BatchId == id);

        var contentType = scanned?.ContentType ?? "application/octet-stream";

        await RecordDocumentAccessAsync(id);

        var stream = await _storage.DownloadAsync(batch.DocumentRef);
        Response.Headers.CacheControl = "private, max-age=300";
        return File(stream, contentType);
    }

    /// <summary>
    /// Lists batches with optional filters and pagination.
    /// </summary>
    [HttpGet]
    [ProducesResponseType(typeof(ApiResponse<BatchListResponse>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> List(
        [FromQuery] string? status,
        [FromQuery] string? batchType,
        [FromQuery] Guid? assignedTo,
        [FromQuery] string? track,
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        [FromQuery] string sortBy = "createdAt",
        [FromQuery] string sortDirection = "desc")
    {
        BatchStatus? parsedStatus = string.IsNullOrEmpty(status) ? null : BatchStatusExtensions.FromDbString(status.ToUpperInvariant());
        BatchType? parsedBatchType = string.IsNullOrEmpty(batchType) ? null : BatchTypeExtensions.FromDbString(batchType.ToUpperInvariant());
        BatchTrack? parsedTrack = string.IsNullOrEmpty(track) ? null : BatchTrackExtensions.FromDbString(track.ToUpperInvariant());

        var result = await _batches.ListAsync(parsedStatus, parsedBatchType, assignedTo, parsedTrack, page, pageSize, sortBy, sortDirection);
        return Ok(ApiResponse<BatchListResponse>.Ok(new BatchListResponse(
            result.Items.Select(b => BatchDetailResponse.FromEntity(b)).ToList(),
            result.Page,
            result.PageSize,
            result.TotalCount,
            result.TotalPages)));
    }

    /// <summary>
    /// Assigns a batch to an entry clerk. Uses Redis lock to prevent double-assignment.
    /// </summary>
    [HttpPatch("{id:guid}/assign")]
    [ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
    public async Task<IActionResult> Assign(Guid id, [FromBody] AssignBatchRequest req)
    {
        var actorUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
        var batch = await _batches.AssignAsync(id, req.EntryClerkUserId, actorUserId);
        return Ok(ApiResponse<BatchDetailResponse>.Ok(BatchDetailResponse.FromEntity(batch)));
    }

    /// <summary>
    /// Cancels a batch permanently. Only batches in UPLOADED, IN_ENTRY, or REJECTED
    /// status can be cancelled. Admin only.
    /// </summary>
    [HttpPost("{id:guid}/cancel")]
    [Authorize(Roles = "ADMINISTRATOR")]
    [ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> Cancel(Guid id, [FromBody] CancelBatchRequest req)
    {
        var actorUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
        var batch = await _batches.CancelAsync(id, req.Reason, actorUserId);
        return Ok(ApiResponse<BatchDetailResponse>.Ok(BatchDetailResponse.FromEntity(batch)));
    }

    /// <summary>
    /// Promotes an approved batch to live clinical data. For correction batches,
    /// marks the original batch's live observations as superseded (append-only).
    /// </summary>
    [HttpPost("{id:guid}/promote")]
    [Authorize(Roles = "CLINICAL_APPROVER,ADMINISTRATOR")]
    [ProducesResponseType(typeof(ApiResponse<PromotionResult>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> Promote(Guid id)
    {
        var actorUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
        var result = await _promotion.PromoteAsync(id, actorUserId);
        return Ok(ApiResponse<PromotionResult>.Ok(result));
    }

    /// <summary>
    /// Returns cursor-paginated audit trail events for a batch.
    /// Events are ordered chronologically (oldest first).
    /// Pass the "after" parameter with the cursor from the previous page to paginate.
    /// </summary>
    /// <param name="id">Batch ID.</param>
    /// <param name="after">Cursor: ISO-8601 timestamp from the previous page's nextCursor field.</param>
    /// <param name="pageSize">Number of events per page. Default 50, max 200.</param>
    [HttpGet("{id:guid}/events")]
    [Authorize(Roles = "ADMINISTRATOR,VERIFIER,CLINICAL_APPROVER")]
    [ProducesResponseType(typeof(ApiResponse<CursorPagedResult<BatchEventResponse>>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetEvents(
        Guid id,
        [FromQuery] string? after = null,
        [FromQuery] int pageSize = 50)
    {
        DateTimeOffset? afterCursor = null;
        if (!string.IsNullOrWhiteSpace(after))
        {
            if (!DateTimeOffset.TryParse(after, out var parsed))
                return BadRequest(ApiResponse<object>.Fail(
                    400,
                    "Invalid cursor format. Expected ISO-8601 timestamp.",
                    "INVALID_CURSOR"));

            afterCursor = parsed;
        }

        var result = await _batchEventService.GetEventsAsync(id, afterCursor, pageSize);
        return Ok(ApiResponse<CursorPagedResult<BatchEventResponse>>.Ok(result));
    }

    private async Task RecordDocumentAccessAsync(Guid batchId)
    {
        var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
        var cutoff = DateTimeOffset.UtcNow.AddMinutes(-5);
        var recentAccess = await _db.DigitizationEvents.AnyAsync(e =>
            e.BatchId == batchId &&
            e.EventType == DigitizationEventType.DocumentAccessed &&
            e.ActorUserId == userId &&
            e.OccurredAt >= cutoff);

        if (recentAccess) return;

        _db.DigitizationEvents.Add(new DigitizationEvent
        {
            Id = Guid.NewGuid(),
            BatchId = batchId,
            EventType = DigitizationEventType.DocumentAccessed,
            ActorUserId = userId,
            OccurredAt = DateTimeOffset.UtcNow
        });
        await _db.SaveChangesAsync();
    }
}