package de.gecheckt.pdf.umbenenner.application.service;

import java.util.Objects;

import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationPort;
import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationSuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationTechnicalFailure;
import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingFailure;
import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingSuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.PromptPort;
import de.gecheckt.pdf.umbenenner.domain.model.AiAttemptContext;
import de.gecheckt.pdf.umbenenner.domain.model.AiFunctionalFailure;
import de.gecheckt.pdf.umbenenner.domain.model.AiRequestRepresentation;
import de.gecheckt.pdf.umbenenner.domain.model.AiResponseParsingFailure;
import de.gecheckt.pdf.umbenenner.domain.model.AiResponseParsingSuccess;
import de.gecheckt.pdf.umbenenner.domain.model.AiTechnicalFailure;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome;
import de.gecheckt.pdf.umbenenner.domain.model.NamingProposal;
import de.gecheckt.pdf.umbenenner.domain.model.NamingProposalReady;
import de.gecheckt.pdf.umbenenner.domain.model.ParsedAiResponse;
import de.gecheckt.pdf.umbenenner.domain.model.PreCheckPassed;
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate;

/**
 * Orchestrates the complete AI naming pipeline for a single document.
 * <p>
 * This service is called after pre-checks have passed (i.e. after extraction
 * and content quality checks) and performs exactly these steps in order:
 * <ol>
 *   <li>Load the external prompt template via {@link PromptPort}.</li>
 *   <li>Limit the extracted document text to the configured maximum character count.</li>
 *   <li>Compose a deterministic AI request from the prompt and limited text.</li>
 *   <li>Invoke the AI service via {@link AiInvocationPort}.</li>
 *   <li>Parse the raw AI response for structural correctness.</li>
 *   <li>Validate the parsed response for semantic correctness (title, date).</li>
 *   <li>Return a typed {@link DocumentProcessingOutcome} encoding success or failure.</li>
 * </ol>
 *
 * <h2>Outcome classification</h2>
 * <ul>
 *   <li>{@link NamingProposalReady} — AI responded with a structurally and semantically
 *       valid naming proposal; document status will become {@code PROPOSAL_READY}.</li>
 *   <li>{@link AiTechnicalFailure} — transient infrastructure problem (prompt load failure,
 *       HTTP error, timeout, connection problem, or unparseable JSON response); transient
 *       error counter is incremented and the document is retryable in a later run.</li>
 *   <li>{@link AiFunctionalFailure} — the AI responded successfully but the content
 *       fails deterministic validation (title too long, prohibited characters, generic
 *       placeholder, unparseable date); content error counter is incremented and the
 *       one-retry rule applies.</li>
 * </ul>
 *
 * <h2>AI traceability</h2>
 * <p>
 * Every returned outcome carries an {@link AiAttemptContext} with the model name,
 * prompt identifier, page count, sent character count, and raw response (null on
 * connection failure). This context is persisted verbatim in the processing attempt
 * history by the coordinator.
 *
 * <h2>Thread safety</h2>
 * <p>
 * This service is stateless with respect to individual documents. It is safe to
 * reuse a single instance across documents within the same batch run, provided the
 * injected dependencies are thread-safe.
 */
public class AiNamingService {

    /**
     * Placeholder token that can appear in the loaded prompt content and is replaced with
     * the configured maximum base title length before the prompt is sent to the AI.
     */
    private static final String MAX_TITLE_LENGTH_PLACEHOLDER = "{MAX_TITLE_LENGTH}";

    private final AiInvocationPort aiInvocationPort;
    private final PromptPort promptPort;
    private final AiResponseValidator aiResponseValidator;
    private final String modelName;
    private final int maxTextCharacters;
    private final int maxTitleLength;

    /**
     * Creates the AI naming service with all required dependencies.
     *
     * @param aiInvocationPort     port for invoking the AI over HTTP; must not be null
     * @param promptPort           port for loading the external prompt template; must not be null
     * @param aiResponseValidator  semantic validator for parsed AI responses; must not be null
     * @param modelName            the AI model name to record in attempt history; must not be null
     * @param maxTextCharacters    the maximum number of document-text characters to send;
     *                             must be &gt;= 1
     * @param maxTitleLength       the configured maximum base title length; must be &gt;= 1.
     *                             Used to replace the {@value #MAX_TITLE_LENGTH_PLACEHOLDER}
     *                             placeholder in the loaded prompt content before the prompt
     *                             is sent to the AI.
     * @throws NullPointerException     if any reference parameter is null
     * @throws IllegalArgumentException if {@code maxTextCharacters} or {@code maxTitleLength}
     *                                  is less than 1
     */
    public AiNamingService(
            AiInvocationPort aiInvocationPort,
            PromptPort promptPort,
            AiResponseValidator aiResponseValidator,
            String modelName,
            int maxTextCharacters,
            int maxTitleLength) {
        this.aiInvocationPort = Objects.requireNonNull(aiInvocationPort, "aiInvocationPort must not be null");
        this.promptPort = Objects.requireNonNull(promptPort, "promptPort must not be null");
        this.aiResponseValidator = Objects.requireNonNull(aiResponseValidator, "aiResponseValidator must not be null");
        this.modelName = Objects.requireNonNull(modelName, "modelName must not be null");
        if (maxTextCharacters < 1) {
            throw new IllegalArgumentException(
                    "maxTextCharacters must be >= 1, but was: " + maxTextCharacters);
        }
        if (maxTitleLength < 1) {
            throw new IllegalArgumentException(
                    "maxTitleLength must be >= 1, but was: " + maxTitleLength);
        }
        this.maxTextCharacters = maxTextCharacters;
        this.maxTitleLength = maxTitleLength;
    }

    /**
     * Runs the AI naming pipeline for a document that passed all pre-checks.
     * <p>
     * The extraction result embedded in {@code preCheckPassed} supplies the
     * document text and page count needed for the AI request. The candidate is
     * carried through for correct outcome construction (correlation, logging).
     *
     * @param preCheckPassed the pre-check result carrying the candidate and extraction;
     *                       must not be null
     * @return a {@link DocumentProcessingOutcome} encoding the AI pipeline result;
     *         one of {@link NamingProposalReady}, {@link AiTechnicalFailure}, or
     *         {@link AiFunctionalFailure}; never null
     * @throws NullPointerException if {@code preCheckPassed} is null
     */
    public DocumentProcessingOutcome invoke(PreCheckPassed preCheckPassed) {
        Objects.requireNonNull(preCheckPassed, "preCheckPassed must not be null");

        SourceDocumentCandidate candidate = preCheckPassed.candidate();
        int pageCount = preCheckPassed.extraction().pageCount().value();
        String rawText = preCheckPassed.extraction().extractedText();

        // Step 1: Load the external prompt template
        return switch (promptPort.loadPrompt()) {
            case PromptLoadingFailure promptFailure ->
                // Prompt is unavailable — transient infrastructure failure; retryable
                new AiTechnicalFailure(
                        candidate,
                        "Prompt loading failed [" + promptFailure.failureReason() + "]: "
                                + promptFailure.failureMessage(),
                        null,
                        new AiAttemptContext(modelName, "prompt-load-failed", pageCount, 0, null));

            case PromptLoadingSuccess promptSuccess ->
                invokeWithPrompt(candidate, rawText, pageCount, promptSuccess);
        };
    }

    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------

    /**
     * Continues the AI pipeline after the prompt has been loaded successfully.
     */
    private DocumentProcessingOutcome invokeWithPrompt(
            SourceDocumentCandidate candidate,
            String rawText,
            int pageCount,
            PromptLoadingSuccess promptSuccess) {

        String promptIdentifier = promptSuccess.promptIdentifier().identifier();
        // Replace the maximum-title-length placeholder with the configured value before the
        // prompt is sent to the AI. Prompts that do not contain the placeholder are passed
        // through unchanged.
        String promptContent = promptSuccess.promptContent()
                .replace(MAX_TITLE_LENGTH_PLACEHOLDER, String.valueOf(maxTitleLength));

        // Step 2: Limit the document text to the configured maximum
        String limitedText = DocumentTextLimiter.limit(rawText, maxTextCharacters);
        int sentCharacterCount = limitedText.length();

        // Step 3: Compose a deterministic AI request
        AiRequestRepresentation request = AiRequestComposer.compose(
                promptSuccess.promptIdentifier(),
                promptContent,
                limitedText);

        // Step 4: Invoke the AI service
        return switch (aiInvocationPort.invoke(request)) {
            case AiInvocationTechnicalFailure invocationFailure ->
                // Transient infrastructure failure: timeout, network error, etc.
                new AiTechnicalFailure(
                        candidate,
                        "AI invocation failed [" + invocationFailure.failureReason() + "]: "
                                + invocationFailure.failureMessage(),
                        null,
                        new AiAttemptContext(
                                modelName, promptIdentifier, pageCount, sentCharacterCount, null));

            case AiInvocationSuccess invocationSuccess ->
                processSuccessfulInvocation(
                        candidate, pageCount, sentCharacterCount, promptIdentifier,
                        invocationSuccess);
        };
    }

    /**
     * Processes a technically successful AI invocation: parses and validates the response.
     */
    private DocumentProcessingOutcome processSuccessfulInvocation(
            SourceDocumentCandidate candidate,
            int pageCount,
            int sentCharacterCount,
            String promptIdentifier,
            AiInvocationSuccess invocationSuccess) {

        String rawResponseBody = invocationSuccess.rawResponse().content();
        de.gecheckt.pdf.umbenenner.application.dto.AiUsageMetadata usage = invocationSuccess.usageMetadata();

        // Step 5: Parse the raw response for structural correctness
        return switch (AiResponseParser.parse(invocationSuccess.rawResponse())) {
            case AiResponseParsingFailure parsingFailure ->
                // Unparseable JSON or structurally invalid response: transient technical error
                new AiTechnicalFailure(
                        candidate,
                        "AI response could not be parsed [" + parsingFailure.failureReason() + "]: "
                                + parsingFailure.failureMessage(),
                        null,
                        new AiAttemptContext(
                                modelName, promptIdentifier, pageCount, sentCharacterCount,
                                rawResponseBody,
                                usage.inputTokens(), usage.outputTokens(),
                                usage.cacheCreationInputTokens(), usage.cacheReadInputTokens()));

            case AiResponseParsingSuccess parsingSuccess ->
                // Step 6: Validate semantics (title rules, date format)
                validateAndBuildOutcome(
                        candidate, pageCount, sentCharacterCount, promptIdentifier,
                        rawResponseBody, parsingSuccess.response(), usage);
        };
    }

    /**
     * Validates the parsed AI response and builds the final outcome.
     *
     * @param usage Token-Verbrauchsmetadaten; nicht {@code null}
     */
    private DocumentProcessingOutcome validateAndBuildOutcome(
            SourceDocumentCandidate candidate,
            int pageCount,
            int sentCharacterCount,
            String promptIdentifier,
            String rawResponseBody,
            ParsedAiResponse parsedResponse,
            de.gecheckt.pdf.umbenenner.application.dto.AiUsageMetadata usage) {

        AiAttemptContext aiContext = new AiAttemptContext(
                modelName, promptIdentifier, pageCount, sentCharacterCount, rawResponseBody,
                usage.inputTokens(), usage.outputTokens(),
                usage.cacheCreationInputTokens(), usage.cacheReadInputTokens());

        return switch (aiResponseValidator.validate(parsedResponse)) {
            case AiResponseValidator.AiValidationResult.Invalid invalid ->
                // Deterministic semantic failure: bad title, bad date, generic placeholder
                new AiFunctionalFailure(candidate, invalid.errorMessage(), aiContext);

            case AiResponseValidator.AiValidationResult.Valid valid -> {
                NamingProposal proposal = valid.proposal();
                yield new NamingProposalReady(candidate, proposal, aiContext);
            }
        };
    }
}
