using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;


/// <summary>
/// Implements batch verification and rejection with separation-of-duties enforcement.
/// Every status transition writes a DigitizationEvent with the actor, timestamp,
/// and metadata including field-level checks where applicable.
/// </summary>
public class VerificationService : IVerificationService
{
    private readonly AppDbContext _db;
    private readonly SiteConfigOptions _siteConfig;
    private readonly ILogger<VerificationService> _logger;

    public VerificationService(
        AppDbContext db,
        IOptions<SiteConfigOptions> siteConfig,
        ILogger<VerificationService> logger)
    {
        _db = db;
        _siteConfig = siteConfig.Value;
        _logger = logger;
    }

    public async Task<DigitizationBatch> VerifyAsync(
        Guid batchId, VerifyBatchRequest request, Guid verifierUserId)
    {
        var batch = await _db.DigitizationBatches
            .Include(b => b.Events)
            .FirstOrDefaultAsync(b => b.Id == batchId);

        if (batch is null)
            throw new NotFoundException("Batch not found.", "BATCH_NOT_FOUND");

        // --- Status guard ---
        if (batch.Status != BatchStatus.PendingVerification)
            throw new ConflictException(
                $"Batch is in '{batch.Status.ToDbString()}' status. " +
                "Only batches in 'PENDING_VERIFICATION' can be verified.",
                "ILLEGAL_STATUS_TRANSITION");

        // --- Separation of duties ---
        if (batch.EnteredByUserId == verifierUserId)
            throw new ConflictException(
                "The user who entered the data cannot verify the same batch. " +
                "Assign a different verifier.",
                "SEPARATION_OF_DUTIES_VIOLATION");

        // --- Validate request ---
        if (request.FieldChecks is null || request.FieldChecks.Count == 0)
            throw new ValidationException(
                "At least one field check is required.",
                "FIELD_CHECKS_REQUIRED");

        var invalidStatuses = request.FieldChecks
            .Where(fc => fc.Status is not ("ok" or "warning" or "error"))
            .Select(fc => fc.FieldName)
            .ToList();

        if (invalidStatuses.Count > 0)
            throw new ValidationException(
                $"Invalid status for fields: {string.Join(", ", invalidStatuses)}. " +
                "Allowed values: ok, warning, error.",
                "INVALID_FIELD_CHECK_STATUS");

        // --- Build event metadata with field checks ---
        var metadata = new
        {
            fieldChecks = request.FieldChecks.Select(fc => new
            {
                fieldName = fc.FieldName,
                status = fc.Status,
                note = fc.Note
            }),
            passed = request.Passed,
            totalChecks = request.FieldChecks.Count,
            errorCount = request.FieldChecks.Count(fc => fc.Status == "error"),
            warningCount = request.FieldChecks.Count(fc => fc.Status == "warning")
        };

        if (request.Passed)
        {
            // --- Determine target status based on site config ---
            var requiresClinical = _siteConfig.RequiresClinicalApproval(batch.BatchType);
            var targetStatus = requiresClinical
                ? BatchStatus.AwaitingClinicalApproval
                : BatchStatus.Verified;

            batch.Status = targetStatus;
            batch.VerifiedByUserId = verifierUserId;
            batch.RejectionReason = null; // Clear any previous rejection reason
            batch.UpdatedAt = DateTimeOffset.UtcNow;

            _db.DigitizationEvents.Add(new DigitizationEvent
            {
                Id = Guid.NewGuid(),
                BatchId = batchId,
                EventType = requiresClinical ? DigitizationEventType.VerifiedPendingClinical : DigitizationEventType.Verified,
                ActorUserId = verifierUserId,
                OccurredAt = DateTimeOffset.UtcNow,
                MetadataJson = JsonSerializer.Serialize(metadata)
            });

            await _db.SaveChangesAsync();

            _logger.LogInformation(
                "Batch {BatchId} verified by {VerifierUserId} -> {TargetStatus}",
                batchId, verifierUserId, targetStatus.ToDbString());
        }
        else
        {
            // Verification failed — treat as rejection with field-level detail
            var fieldErrors = request.FieldChecks
                .Where(fc => fc.Status == "error")
                .Select(fc => $"{fc.FieldName}: {fc.Note ?? "failed check"}")
                .ToList();

            var rejectionReason = fieldErrors.Count > 0
                ? $"Verification failed. Errors: {string.Join("; ", fieldErrors)}"
                : "Verification failed. See field checks for details.";

            batch.Status = BatchStatus.Rejected;
            batch.RejectionReason = rejectionReason;
            batch.VerifiedByUserId = null;
            batch.UpdatedAt = DateTimeOffset.UtcNow;

            _db.DigitizationEvents.Add(new DigitizationEvent
            {
                Id = Guid.NewGuid(),
                BatchId = batchId,
                EventType = DigitizationEventType.VerificationFailed,
                ActorUserId = verifierUserId,
                OccurredAt = DateTimeOffset.UtcNow,
                MetadataJson = JsonSerializer.Serialize(metadata)
            });

            await _db.SaveChangesAsync();

            DiagnosticsMetrics.RejectionTotal
                .WithLabels("verification_failed")
                .Inc();

            _logger.LogInformation(
                "Batch {BatchId} verification failed by {VerifierUserId}, rejected",
                batchId, verifierUserId);
        }

        return batch;
    }

    public async Task<DigitizationBatch> RejectAsync(
        Guid batchId, RejectBatchRequest request, Guid actorUserId)
    {
        var batch = await _db.DigitizationBatches
            .FirstOrDefaultAsync(b => b.Id == batchId);

        if (batch is null)
            throw new NotFoundException("Batch not found.", "BATCH_NOT_FOUND");

        // --- Status guard: reject is allowed from PendingVerification or AwaitingClinicalApproval ---
        if (batch.Status is not (BatchStatus.PendingVerification or BatchStatus.AwaitingClinicalApproval))
            throw new ConflictException(
                $"Batch is in '{batch.Status.ToDbString()}' status. " +
                "Only batches in 'PENDING_VERIFICATION' or 'AWAITING_CLINICAL_APPROVAL' can be rejected.",
                "ILLEGAL_STATUS_TRANSITION");

        // --- Separation of duties for rejection from PendingVerification ---
        if (batch.Status == BatchStatus.PendingVerification && batch.EnteredByUserId == actorUserId)
            throw new ConflictException(
                "The user who entered the data cannot reject the same batch. " +
                "Assign a different verifier.",
                "SEPARATION_OF_DUTIES_VIOLATION");

        // --- Validate reason ---
        if (string.IsNullOrWhiteSpace(request.Reason))
            throw new ValidationException(
                "Rejection reason is required.",
                "REJECTION_REASON_REQUIRED");

        if (request.Reason.Length < 10)
            throw new ValidationException(
                "Rejection reason must be at least 10 characters.",
                "REJECTION_REASON_TOO_SHORT");

        // --- Apply rejection ---
        var previousStatus = batch.Status;
        batch.Status = BatchStatus.Rejected;
        batch.RejectionReason = request.Reason;
        batch.VerifiedByUserId = null;
        batch.UpdatedAt = DateTimeOffset.UtcNow;

        var metadata = new
        {
            reason = request.Reason,
            previousStatus = previousStatus.ToDbString(),
            rejectedByUserId = actorUserId
        };

        _db.DigitizationEvents.Add(new DigitizationEvent
        {
            Id = Guid.NewGuid(),
            BatchId = batchId,
            EventType = DigitizationEventType.Rejected,
            ActorUserId = actorUserId,
            OccurredAt = DateTimeOffset.UtcNow,
            MetadataJson = JsonSerializer.Serialize(metadata)
        });

        await _db.SaveChangesAsync();

        var category = previousStatus == BatchStatus.AwaitingClinicalApproval
            ? "clinical_rejected"
            : "verification_failed";
        DiagnosticsMetrics.RejectionTotal
            .WithLabels(category)
            .Inc();

        _logger.LogInformation(
            "Batch {BatchId} rejected by {ActorUserId} from {PreviousStatus}: {Reason}",
            batchId, actorUserId, previousStatus.ToDbString(), request.Reason);

        return batch;
    }
}