package de.gecheckt.pdf.umbenenner.adapter.out.targetcopy;

import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Objects;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileCopyPort;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileCopyResult;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileCopySuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileCopyTechnicalFailure;
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator;

/**
 * Filesystem-based implementation of {@link TargetFileCopyPort}.
 * <p>
 * Copies a source PDF to the configured target folder using a two-step approach:
 * <ol>
 *   <li>Write the source content to a temporary file in the target folder.</li>
 *   <li>Rename/move the temporary file to the final resolved filename.</li>
 * </ol>
 * The atomic-move option is attempted first. If the filesystem does not support atomic
 * moves (e.g., across different volumes), a standard move is used as a fallback.
 *
 * <h2>Source integrity</h2>
 * <p>
 * The source file is never modified, moved, or deleted. Only a copy is created.
 *
 * <h2>Temporary file naming</h2>
 * <p>
 * The temporary file uses the suffix {@code .tmp} appended to the resolved filename
 * and is placed in the same target folder. This ensures the final rename is typically
 * an intra-filesystem operation, maximising atomicity.
 *
 * <h2>Architecture boundary</h2>
 * <p>
 * All NIO operations are confined to this adapter. No {@code Path} or {@code File}
 * types appear in the port interface.
 */
public class FilesystemTargetFileCopyAdapter implements TargetFileCopyPort {

    private static final Logger logger = LogManager.getLogger(FilesystemTargetFileCopyAdapter.class);

    private final Path targetFolderPath;

    /**
     * Creates the adapter for the given target folder.
     *
     * @param targetFolderPath the target folder path; must not be null
     * @throws NullPointerException if {@code targetFolderPath} is null
     */
    public FilesystemTargetFileCopyAdapter(Path targetFolderPath) {
        this.targetFolderPath = Objects.requireNonNull(targetFolderPath, "targetFolderPath must not be null");
    }

    /**
     * Copies the source document to the target folder under the given resolved filename.
     * <p>
     * The copy is performed via a temporary file ({@code resolvedFilename + ".tmp"}) in
     * the target folder followed by a move/rename to the final name.
     * <p>
     * If any step fails, a best-effort cleanup of the temporary file is attempted
     * before returning the failure result.
     *
     * @param sourceLocator    opaque locator identifying the source file; must not be null
     * @param resolvedFilename the final filename in the target folder; must not be null or blank
     * @return {@link TargetFileCopySuccess} on success, or
     *         {@link TargetFileCopyTechnicalFailure} on any failure
     */
    @Override
    public TargetFileCopyResult copyToTarget(SourceDocumentLocator sourceLocator, String resolvedFilename) {
        Objects.requireNonNull(sourceLocator, "sourceLocator must not be null");
        Objects.requireNonNull(resolvedFilename, "resolvedFilename must not be null");

        Path sourcePath = Paths.get(sourceLocator.value());
        Path finalTargetPath = targetFolderPath.resolve(resolvedFilename);
        Path tempTargetPath = targetFolderPath.resolve(resolvedFilename + ".tmp");

        boolean tempCreated = false;

        try {
            // Step 1: Copy source to temporary file in target folder
            Files.copy(sourcePath, tempTargetPath, StandardCopyOption.REPLACE_EXISTING);
            tempCreated = true;
            logger.debug("Copied source '{}' to temporary file '{}'.",
                    sourceLocator.value(), tempTargetPath.getFileName());

            // Step 2: Atomic move/rename to final target filename
            moveToFinalTarget(tempTargetPath, finalTargetPath);

            logger.debug("Target copy completed: '{}'.", resolvedFilename);
            return new TargetFileCopySuccess();

        } catch (Exception e) {
            String message = "Failed to copy source '" + sourceLocator.value()
                    + "' to target '" + resolvedFilename + "': " + e.getMessage();
            logger.error(message, e);

            boolean cleaned = tempCreated && tryDeletePath(tempTargetPath);
            return new TargetFileCopyTechnicalFailure(message, cleaned);
        }
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    /**
     * Moves the temporary file to the final target path.
     * Attempts an atomic move first; falls back to a standard move if the filesystem
     * does not support atomic moves.
     */
    private void moveToFinalTarget(Path tempPath, Path finalPath) throws IOException {
        try {
            Files.move(tempPath, finalPath, StandardCopyOption.ATOMIC_MOVE);
        } catch (AtomicMoveNotSupportedException e) {
            logger.debug("Atomic move not supported, falling back to standard move.");
            Files.move(tempPath, finalPath, StandardCopyOption.REPLACE_EXISTING);
        }
    }

    /**
     * Best-effort deletion of a path. Returns {@code true} if deletion succeeded
     * or the file did not exist; {@code false} if an exception occurred.
     */
    private boolean tryDeletePath(Path path) {
        try {
            Files.deleteIfExists(path);
            return true;
        } catch (IOException e) {
            logger.warn("Best-effort cleanup: could not delete temporary file '{}': {}",
                    path, e.getMessage());
            return false;
        }
    }
}
