package de.gecheckt.pdf.umbenenner.bootstrap.e2e;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import de.gecheckt.pdf.umbenenner.adapter.out.clock.SystemClockAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.fingerprint.Sha256FingerprintAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.lock.FilesystemRunLockPortAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.pdfextraction.PdfTextExtractionPortAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.prompt.FilesystemPromptPortAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.sourcedocument.SourceDocumentCandidatesPortAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteDocumentRecordRepositoryAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteProcessingAttemptRepositoryAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteSchemaInitializationAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteUnitOfWorkAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.targetcopy.FilesystemTargetFileCopyAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.targetfolder.FilesystemTargetFolderAdapter;
import de.gecheckt.pdf.umbenenner.application.config.RuntimeConfiguration;
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentKnownProcessable;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalFinalFailure;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalSuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintPort;
import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintSuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.PdfTextExtractionPort;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttemptRepository;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
import de.gecheckt.pdf.umbenenner.application.port.out.PromptPort;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentCandidatesPort;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileCopyPort;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderPort;
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
import de.gecheckt.pdf.umbenenner.application.service.AiNamingService;
import de.gecheckt.pdf.umbenenner.application.service.AiResponseValidator;
import de.gecheckt.pdf.umbenenner.application.service.DocumentProcessingCoordinator;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultBatchRunProcessingUseCase;
import de.gecheckt.pdf.umbenenner.bootstrap.adapter.Log4jProcessingLogger;
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
import de.gecheckt.pdf.umbenenner.domain.model.RunId;
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate;
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator;

/**
 * Full adapter wiring context for deterministic end-to-end tests of the batch processing pipeline.
 * <p>
 * Provides real infrastructure adapters for all subsystems (SQLite persistence, filesystem
 * source/target folders, PDF text extraction, SHA-256 fingerprinting, run locking) and a
 * configurable stub ({@link StubAiInvocationPort}) for the AI invocation port.
 * This ensures that end-to-end tests cover the complete production code path without
 * performing real HTTP calls to an AI service.
 *
 * <h2>Invariants verified by this context</h2>
 * <ul>
 *   <li>Happy-path to {@code SUCCESS}: two-run flow where Run 1 produces {@code PROPOSAL_READY}
 *       and Run 2 copies the file and records {@code SUCCESS}.</li>
 *   <li>Deterministic content error: blank PDFs (no extractable text) produce
 *       {@code FAILED_RETRYABLE} after the first run and {@code FAILED_FINAL} after a
 *       second run.</li>
 *   <li>Transient technical error: AI stub failures produce {@code FAILED_RETRYABLE} for each
 *       run until the transient error limit is reached, at which point the document is
 *       finalized to {@code FAILED_FINAL}.</li>
 *   <li>Skip after {@code SUCCESS}: a document in {@code SUCCESS} state generates a
 *       {@code SKIPPED_ALREADY_PROCESSED} attempt in subsequent runs.</li>
 *   <li>Skip after {@code FAILED_FINAL}: a document in {@code FAILED_FINAL} state generates a
 *       {@code SKIPPED_FINAL_FAILURE} attempt in subsequent runs.</li>
 *   <li>{@code PROPOSAL_READY} with later finalization: a document in {@code PROPOSAL_READY}
 *       state is finalized without an AI call in the next run.</li>
 *   <li>Target copy error with immediate retry: when the first copy attempt fails but the
 *       immediate within-run retry succeeds, the document is recorded as {@code SUCCESS}.</li>
 * </ul>
 *
 * <h2>Usage pattern</h2>
 * <pre>{@code
 * @TempDir Path tempDir;
 *
 * @Test
 * void example() throws Exception {
 *     try (E2ETestContext ctx = E2ETestContext.initialize(tempDir)) {
 *         ctx.createSearchablePdf("doc.pdf", "Rechnung 2024-01-15 ...");
 *         BatchRunOutcome run1 = ctx.runBatch();
 *         // assertions...
 *     }
 * }
 * }</pre>
 *
 * <h2>Thread safety</h2>
 * <p>
 * Not thread-safe. Each test method should use its own context instance.
 */
public final class E2ETestContext implements AutoCloseable {

    /** Maximum pages before triggering a deterministic content error. */
    static final int MAX_PAGES = 50;

    /** Maximum text characters sent to the AI service. */
    static final int MAX_TEXT_CHARS = 10_000;

    /**
     * Maximum transient retries before a document is finalized to {@code FAILED_FINAL}.
     * Set to 3 to allow multi-run transient-failure tests without immediate finalization.
     */
    static final int MAX_RETRIES_TRANSIENT = 3;

    /** Default maximum base title length used in the E2E test context. */
    static final int MAX_TITLE_LENGTH = 60;

    /** Model name carried in attempt history (no real inference occurs). */
    static final String AI_MODEL = "e2e-stub-model";

    private final Path sourceFolder;
    private final Path targetFolder;
    private final Path lockFile;
    private final Path promptFile;
    private final String jdbcUrl;

    private final SqliteDocumentRecordRepositoryAdapter documentRepo;
    private final SqliteProcessingAttemptRepositoryAdapter attemptRepo;

    /**
     * Configurable AI stub. Tests may call {@link StubAiInvocationPort#configureSuccess},
     * {@link StubAiInvocationPort#configureTechnicalFailure}, or
     * {@link StubAiInvocationPort#reset()} between batch runs.
     */
    public final StubAiInvocationPort aiStub;

    /**
     * Optional override for the {@link TargetFileCopyPort}.
     * {@code null} means the real {@link FilesystemTargetFileCopyAdapter} is used.
     * Set via {@link #setTargetFileCopyPortOverride} to inject a failure-simulating stub.
     */
    private TargetFileCopyPort targetFileCopyPortOverride;

    /** Provider identifier written into the attempt history for each batch run. */
    private final String providerIdentifier;

    private E2ETestContext(
            Path sourceFolder,
            Path targetFolder,
            Path lockFile,
            Path promptFile,
            String jdbcUrl,
            SqliteDocumentRecordRepositoryAdapter documentRepo,
            SqliteProcessingAttemptRepositoryAdapter attemptRepo,
            StubAiInvocationPort aiStub,
            String providerIdentifier) {
        this.sourceFolder = sourceFolder;
        this.targetFolder = targetFolder;
        this.lockFile = lockFile;
        this.promptFile = promptFile;
        this.jdbcUrl = jdbcUrl;
        this.documentRepo = documentRepo;
        this.attemptRepo = attemptRepo;
        this.aiStub = aiStub;
        this.providerIdentifier = providerIdentifier;
    }

    /**
     * Initializes a fully wired end-to-end test context rooted in {@code tempDir},
     * using the default provider identifier {@code "openai-compatible"}.
     *
     * @param tempDir the JUnit {@code @TempDir} or any writable temporary directory
     * @return a ready-to-use context; caller is responsible for closing it
     * @throws Exception if schema initialization or directory/file creation fails
     */
    public static E2ETestContext initialize(Path tempDir) throws Exception {
        return initializeWithProvider(tempDir, "openai-compatible");
    }

    /**
     * Initializes a fully wired end-to-end test context rooted in {@code tempDir} with
     * a configurable provider identifier written into each attempt's history record.
     * <p>
     * Creates the {@code source/}, {@code target/} subdirectories and a minimal prompt
     * file, initializes the SQLite schema, and wires all adapters.
     *
     * @param tempDir            the JUnit {@code @TempDir} or any writable temporary directory
     * @param providerIdentifier the provider identifier stored in {@code ai_provider} for each
     *                           attempt (e.g. {@code "openai-compatible"} or {@code "claude"})
     * @return a ready-to-use context; caller is responsible for closing it
     * @throws Exception if schema initialization or directory/file creation fails
     */
    public static E2ETestContext initializeWithProvider(Path tempDir, String providerIdentifier)
            throws Exception {
        Path sourceFolder = Files.createDirectories(tempDir.resolve("source"));
        Path targetFolder = Files.createDirectories(tempDir.resolve("target"));
        Path lockFile = tempDir.resolve("run.lock");
        Path promptFile = tempDir.resolve("prompt.txt");

        Files.writeString(promptFile,
                "Analysiere das folgende Dokument und liefere Datum, Titel und Begruendung als JSON-Objekt.");

        String jdbcUrl = "jdbc:sqlite:" + tempDir.resolve("test.db").toAbsolutePath().toString().replace('\\', '/');

        SqliteSchemaInitializationAdapter schema = new SqliteSchemaInitializationAdapter(jdbcUrl);
        schema.initializeSchema();

        SqliteDocumentRecordRepositoryAdapter documentRepo =
                new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
        SqliteProcessingAttemptRepositoryAdapter attemptRepo =
                new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl);

        return new E2ETestContext(
                sourceFolder, targetFolder, lockFile, promptFile,
                jdbcUrl, documentRepo, attemptRepo, new StubAiInvocationPort(),
                providerIdentifier);
    }

    // =========================================================================
    // Test fixture creation
    // =========================================================================

    /**
     * Creates a single-page searchable PDF in the source folder with the given text.
     * <p>
     * The file is ready for the batch run as soon as this method returns.
     *
     * @param filename    the PDF filename (e.g. {@code "rechnung.pdf"})
     * @param textContent text to embed; should be at least a few words to pass pre-checks
     * @return the absolute path of the created file
     * @throws IOException if the file cannot be written
     */
    public Path createSearchablePdf(String filename, String textContent) throws IOException {
        Path pdfPath = sourceFolder.resolve(filename);
        MinimalPdfFactory.createSearchablePdf(pdfPath, textContent);
        return pdfPath;
    }

    /**
     * Creates a single-page blank PDF (no extractable text) in the source folder.
     * <p>
     * Processing this file triggers the "no usable text" deterministic content error,
     * which skips the AI call.
     *
     * @param filename the PDF filename (e.g. {@code "blank.pdf"})
     * @return the absolute path of the created file
     * @throws IOException if the file cannot be written
     */
    public Path createBlankPdf(String filename) throws IOException {
        Path pdfPath = sourceFolder.resolve(filename);
        MinimalPdfFactory.createBlankPdf(pdfPath);
        return pdfPath;
    }

    // =========================================================================
    // Batch execution
    // =========================================================================

    /**
     * Executes one complete batch run using the current stub configuration.
     * <p>
     * A fresh {@link BatchRunContext} with a new {@link RunId} is created for each call,
     * matching the production behavior where every Task Scheduler invocation is a
     * distinct run.
     *
     * @return the outcome of the batch run
     */
    public BatchRunOutcome runBatch() {
        DefaultBatchRunProcessingUseCase useCase = buildUseCase();
        BatchRunContext context = new BatchRunContext(
                new RunId(UUID.randomUUID().toString()), Instant.now());
        return useCase.execute(context);
    }

    // =========================================================================
    // State inspection helpers
    // =========================================================================

    /**
     * Looks up the document master record for the given fingerprint.
     *
     * @param fingerprint the document fingerprint to query
     * @return the master record if one exists, {@link Optional#empty()} if unknown or
     *         if a persistence lookup error occurred
     */
    public Optional<DocumentRecord> findDocumentRecord(DocumentFingerprint fingerprint) {
        return switch (documentRepo.findByFingerprint(fingerprint)) {
            case DocumentTerminalSuccess s -> Optional.of(s.record());
            case DocumentTerminalFinalFailure f -> Optional.of(f.record());
            case DocumentKnownProcessable p -> Optional.of(p.record());
            default -> Optional.empty();
        };
    }

    /**
     * Returns all processing attempts for the given fingerprint in insertion order.
     *
     * @param fingerprint the document fingerprint to query
     * @return all recorded attempts; empty list if none exist
     */
    public List<ProcessingAttempt> findAttempts(DocumentFingerprint fingerprint) {
        return attemptRepo.findAllByFingerprint(fingerprint);
    }

    /**
     * Computes the SHA-256 fingerprint for the given file using the production adapter.
     * <p>
     * Useful for correlating a test PDF with its database record after a batch run.
     *
     * @param file the absolute path of the file to fingerprint
     * @return the fingerprint
     * @throws IllegalStateException if fingerprint computation fails
     */
    public DocumentFingerprint computeFingerprint(Path file) {
        Sha256FingerprintAdapter adapter = new Sha256FingerprintAdapter();
        // Construct a minimal candidate that mirrors how the production source adapter creates one
        SourceDocumentCandidate candidate = new SourceDocumentCandidate(
                file.getFileName().toString(),
                0L,
                new SourceDocumentLocator(file.toAbsolutePath().toString()));
        return switch (adapter.computeFingerprint(candidate)) {
            case FingerprintSuccess s -> s.fingerprint();
            default -> throw new IllegalStateException(
                    "Fingerprint computation failed for test fixture: " + file);
        };
    }

    /**
     * Lists the filenames of all files currently in the target folder.
     *
     * @return list of filenames; empty if target folder is empty
     * @throws IOException if the target folder cannot be read
     */
    public List<String> listTargetFiles() throws IOException {
        try (var stream = Files.list(targetFolder)) {
            return stream.map(p -> p.getFileName().toString()).sorted().toList();
        }
    }

    /**
     * Returns the source folder path used by this context.
     */
    public Path sourceFolder() {
        return sourceFolder;
    }

    /**
     * Returns the target folder path used by this context.
     */
    public Path targetFolder() {
        return targetFolder;
    }

    /**
     * Overrides the {@link TargetFileCopyPort} used in subsequent batch runs.
     * Pass {@code null} to revert to the real {@link FilesystemTargetFileCopyAdapter}.
     *
     * @param override the port implementation to use, or {@code null} for the real adapter
     */
    public void setTargetFileCopyPortOverride(TargetFileCopyPort override) {
        this.targetFileCopyPortOverride = override;
    }

    @Override
    public void close() {
        // No explicit cleanup needed: @TempDir removes all files automatically
    }

    // =========================================================================
    // Private wiring
    // =========================================================================

    /**
     * Constructs a fully wired {@link DefaultBatchRunProcessingUseCase} for a single batch run.
     * <p>
     * All adapters are instantiated fresh per run to avoid shared mutable state between
     * runs (e.g. locks, connection states). The AI stub and optional copy-port override
     * are re-used across runs within the same test.
     */
    private DefaultBatchRunProcessingUseCase buildUseCase() {
        RuntimeConfiguration runtimeConfig = new RuntimeConfiguration(
                MAX_PAGES, MAX_RETRIES_TRANSIENT, AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);

        FingerprintPort fingerprintPort = new Sha256FingerprintAdapter();

        DocumentRecordRepository documentRecordRepository = documentRepo;
        ProcessingAttemptRepository processingAttemptRepository = attemptRepo;
        UnitOfWorkPort unitOfWorkPort = new SqliteUnitOfWorkAdapter(jdbcUrl);

        ProcessingLogger coordinatorLogger = new Log4jProcessingLogger(
                DocumentProcessingCoordinator.class);
        TargetFolderPort targetFolderPort = new FilesystemTargetFolderAdapter(targetFolder);
        TargetFileCopyPort targetFileCopyPort = (targetFileCopyPortOverride != null)
                ? targetFileCopyPortOverride
                : new FilesystemTargetFileCopyAdapter(targetFolder);

        DocumentProcessingCoordinator coordinator = new DocumentProcessingCoordinator(
                documentRecordRepository,
                processingAttemptRepository,
                unitOfWorkPort,
                targetFolderPort,
                targetFileCopyPort,
                coordinatorLogger,
                MAX_RETRIES_TRANSIENT,
                MAX_TITLE_LENGTH,
                providerIdentifier);

        PromptPort promptPort = new FilesystemPromptPortAdapter(promptFile);
        ClockPort clockPort = new SystemClockAdapter();
        AiResponseValidator aiResponseValidator = new AiResponseValidator(clockPort, MAX_TITLE_LENGTH);
        AiNamingService aiNamingService = new AiNamingService(
                aiStub, promptPort, aiResponseValidator, AI_MODEL, MAX_TEXT_CHARS, MAX_TITLE_LENGTH);

        ProcessingLogger useCaseLogger = new Log4jProcessingLogger(
                DefaultBatchRunProcessingUseCase.class);

        RunLockPort runLockPort = new FilesystemRunLockPortAdapter(lockFile);
        SourceDocumentCandidatesPort candidatesPort =
                new SourceDocumentCandidatesPortAdapter(sourceFolder);
        PdfTextExtractionPort extractionPort = new PdfTextExtractionPortAdapter();

        return new DefaultBatchRunProcessingUseCase(
                runtimeConfig,
                runLockPort,
                candidatesPort,
                extractionPort,
                fingerprintPort,
                coordinator,
                aiNamingService,
                useCaseLogger);
    }
}
