using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Npgsql;
using Tiku.Application.Learning;
using Tiku.Domain.Learning;

namespace Tiku.Infrastructure.Learning;

public sealed partial class LearningActivityService
{
    public async Task<AnswerRecordItem> SubmitAnswerAsync(
        LearningActor actor,
        SubmitAnswerCommand command,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(command.IdempotencyKey))
            throw new LearningValidationException("idempotency_key_required", "An idempotency key is required.");
        if (command.SelectedOptionIndices?.Any(index => index < 0) == true)
            throw new LearningValidationException("selected_option_index_invalid",
                "Selected option indices must be zero-based non-negative values.");

        var now = DateTimeOffset.UtcNow;
        var sessionQuestion = await dbContext.PracticeSessionQuestions.SingleOrDefaultAsync(item =>
            item.TenantId == actor.TenantId && item.Id == command.SessionQuestionId, cancellationToken);

        if (sessionQuestion is null)
            throw new LearningResourceNotFoundException(
                "session_question_not_found",
                "An active practice session question was not found.");

        var session = await dbContext.PracticeSessions.SingleOrDefaultAsync(item =>
            item.TenantId == actor.TenantId &&
            item.UserId == actor.UserId &&
            item.Id == sessionQuestion.PracticeSessionId, cancellationToken);
        if (session is null)
            throw new LearningResourceNotFoundException("practice_session_not_found",
                "Practice session was not found.");

        var requestHash = HashAnswer(command);
        var existingOperation = await dbContext.LearningOperationIdempotencies.AsNoTracking().SingleOrDefaultAsync(
            item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId &&
                item.PracticeSessionId == session.Id &&
                item.OperationType == "answer" &&
                item.IdempotencyKey == command.IdempotencyKey, cancellationToken);
        if (existingOperation is not null)
        {
            if (!string.Equals(existingOperation.RequestHash, requestHash, StringComparison.Ordinal))
            {
                AnswerConflicts.Add(1);
                throw new LearningValidationException("idempotency_conflict",
                    "The idempotency key was used with a different request.");
            }

            IdempotencyReplays.Add(1);
            return existingOperation.ResponseSnapshot.Deserialize<AnswerRecordItem>()
                   ?? throw new InvalidOperationException("The stored answer response is invalid.");
        }

        if (session.ExpiresAt.HasValue && session.ExpiresAt <= now)
        {
            session.Status = PracticeSessionStatus.Expired;
            session.Version++;
            await dbContext.SaveChangesAsync(cancellationToken);
            throw new LearningValidationException("practice_session_expired", "The practice session has expired.");
        }

        EnsureAnswerSessionState(session, command);
        var current = await dbContext.AnswerRecords.SingleOrDefaultAsync(answer =>
            answer.TenantId == actor.TenantId &&
            answer.UserId == actor.UserId &&
            answer.PracticeSessionId == session.Id &&
            answer.SessionQuestionId == sessionQuestion.Id &&
            answer.IsCurrent, cancellationToken);
        if (current is not null) current.IsCurrent = false;

        var selectedIndices = command.SelectedOptionIndices?.Distinct().Order().ToArray() ?? [];
        var score = sessionQuestion.Score ?? 1;
        QuestionGradingResult grading;
        try
        {
            grading = QuestionGrader.Grade(new QuestionGradingInput(
                sessionQuestion.QuestionType,
                sessionQuestion.CorrectOptionIndexSnapshot,
                sessionQuestion.CorrectOptionIndicesSnapshot,
                sessionQuestion.AnswerTextSnapshot,
                sessionQuestion.GradingRulesSnapshot,
                selectedIndices,
                command.AnswerText,
                score));
        }
        catch (InvalidOperationException exception)
        {
            ScoringFailures.Add(1);
            logger.LogWarning(exception,
                "Question scoring failed for tenant {TenantId}, session {PracticeSessionId}, question {SessionQuestionId}",
                actor.TenantId, session.Id, sessionQuestion.Id);
            throw new LearningValidationException("question_grading_rule_invalid", exception.Message);
        }

        var record = new AnswerRecord
        {
            TenantId = actor.TenantId,
            UserId = actor.UserId,
            PracticeSessionId = sessionQuestion.PracticeSessionId,
            SessionQuestionId = sessionQuestion.Id,
            SelectedOptions = JsonSerializer.SerializeToElement(selectedIndices),
            AnswerText = command.AnswerText,
            IsCorrect = grading.IsCorrect,
            GradingStatus = grading.Status,
            AwardedScore = grading.AwardedScore,
            Revision = (current?.Revision ?? 0) + 1,
            ClientSequence = command.ClientSequence,
            IdempotencyKey = command.IdempotencyKey.Trim(),
            RequestHash = requestHash,
            IsCurrent = true,
            AnsweredAt = now,
            CreatedAt = now
        };
        dbContext.AnswerRecords.Add(record);
        session.Version++;
        session.LastClientSequence = command.ClientSequence;
        var response = ToItem(record, session.Version);
        dbContext.LearningOperationIdempotencies.Add(new LearningOperationIdempotency
        {
            TenantId = actor.TenantId,
            UserId = actor.UserId,
            PracticeSessionId = session.Id,
            OperationType = "answer",
            IdempotencyKey = command.IdempotencyKey.Trim(),
            RequestHash = requestHash,
            ResponseSnapshot = JsonSerializer.SerializeToElement(response),
            CompletedAt = now
        });

        try
        {
            await dbContext.SaveChangesAsync(cancellationToken);
        }
        catch (DbUpdateConcurrencyException)
        {
            throw new LearningValidationException("practice_session_version_conflict",
                "The practice session changed. Reload it before answering.");
        }
        catch (DbUpdateException exception) when (
            exception.InnerException is PostgresException postgresException &&
            postgresException.SqlState == PostgresErrorCodes.UniqueViolation)
        {
            dbContext.ChangeTracker.Clear();
            var replay = await dbContext.LearningOperationIdempotencies.AsNoTracking().SingleOrDefaultAsync(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId &&
                item.PracticeSessionId == session.Id &&
                item.OperationType == "answer" &&
                item.IdempotencyKey == command.IdempotencyKey, cancellationToken);
            if (replay is not null && string.Equals(replay.RequestHash, requestHash, StringComparison.Ordinal))
            {
                IdempotencyReplays.Add(1);
                return replay.ResponseSnapshot.Deserialize<AnswerRecordItem>()
                       ?? throw new InvalidOperationException("The stored answer response is invalid.");
            }

            AnswerConflicts.Add(1);
            throw new LearningValidationException("practice_answer_conflict",
                "The answer conflicted with another client operation.");
        }

        return response;
    }
}