package net.siegeln.cameleer.saas.provisioning;

import net.siegeln.cameleer.saas.certificate.CertificateInfo;
import net.siegeln.cameleer.saas.certificate.CertificateManager;
import net.siegeln.cameleer.saas.certificate.CertValidationResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HexFormat;
import java.util.List;

public class DockerCertificateManager implements CertificateManager {

    private static final Logger log = LoggerFactory.getLogger(DockerCertificateManager.class);

    private final Path certsDir;

    public DockerCertificateManager(Path certsDir) {
        this.certsDir = certsDir;
    }

    public Path getCertsDir() {
        return certsDir;
    }

    @Override
    public boolean isAvailable() {
        return Files.isDirectory(certsDir) && Files.isWritable(certsDir);
    }

    @Override
    public CertificateInfo getActive() {
        return readCertInfo(certsDir.resolve("cert.pem"), certsDir.resolve("ca.pem"));
    }

    @Override
    public CertificateInfo getStaged() {
        return readCertInfo(certsDir.resolve("staged/cert.pem"), certsDir.resolve("staged/ca.pem"));
    }

    @Override
    public CertificateInfo getArchived() {
        return readCertInfo(certsDir.resolve("prev/cert.pem"), certsDir.resolve("prev/ca.pem"));
    }

    @Override
    public CertValidationResult stage(byte[] certPem, byte[] keyPem, byte[] caBundlePem, String keyPassword) {
        List<String> errors = new ArrayList<>();

        // Parse certificate
        X509Certificate cert;
        try {
            cert = parseCertificate(certPem);
        } catch (Exception e) {
            errors.add("Invalid certificate PEM: " + e.getMessage());
            return CertValidationResult.fail(errors);
        }

        // Parse private key (may be password-protected)
        java.security.PrivateKey privateKey;
        byte[] decryptedKeyPem = keyPem;
        try {
            privateKey = parsePrivateKey(keyPem, keyPassword);
            // Re-encode as unencrypted PKCS8 PEM for Traefik (which needs cleartext)
            if (isEncryptedKey(keyPem)) {
                decryptedKeyPem = encodePrivateKeyPem(privateKey);
            }
            // Verify key matches cert
            if (cert.getPublicKey() instanceof RSAPublicKey rsaPub
                    && privateKey instanceof RSAPrivateKey rsaPriv) {
                if (!rsaPub.getModulus().equals(rsaPriv.getModulus())) {
                    errors.add("Private key does not match certificate");
                }
            }
        } catch (Exception e) {
            errors.add("Invalid private key PEM: " + e.getMessage());
        }

        // Parse CA bundle if provided
        if (caBundlePem != null && caBundlePem.length > 0) {
            try {
                var certs = parseCertificates(caBundlePem);
                if (certs.isEmpty()) {
                    errors.add("CA bundle contains no valid certificates");
                }
            } catch (Exception e) {
                errors.add("Invalid CA bundle PEM: " + e.getMessage());
            }
        }

        if (!errors.isEmpty()) {
            return CertValidationResult.fail(errors);
        }

        // Write to staged directory
        try {
            Path stagedDir = certsDir.resolve("staged");
            Files.createDirectories(stagedDir);

            writeAtomic(stagedDir.resolve("cert.pem"), certPem);
            writeAtomicRestricted(stagedDir.resolve("key.pem"), decryptedKeyPem);
            if (caBundlePem != null && caBundlePem.length > 0) {
                writeAtomic(stagedDir.resolve("ca.pem"), caBundlePem);
            } else {
                Files.deleteIfExists(stagedDir.resolve("ca.pem"));
            }
        } catch (IOException e) {
            log.error("Failed to write staged certificate files", e);
            return CertValidationResult.fail(List.of("Failed to write staged files: " + e.getMessage()));
        }

        var info = toCertInfo(cert, caBundlePem != null && caBundlePem.length > 0, false);
        return CertValidationResult.ok(info);
    }

    @Override
    public void activate() {
        try {
            Path stagedDir = certsDir.resolve("staged");
            Path prevDir = certsDir.resolve("prev");

            if (!Files.exists(stagedDir.resolve("cert.pem"))) {
                throw new IllegalStateException("No staged certificate to activate");
            }

            // Delete existing archive
            deleteDirectory(prevDir);

            // Move current active -> prev (archive)
            if (Files.exists(certsDir.resolve("cert.pem"))) {
                Files.createDirectories(prevDir);
                moveFile(certsDir.resolve("cert.pem"), prevDir.resolve("cert.pem"));
                moveFile(certsDir.resolve("key.pem"), prevDir.resolve("key.pem"));
                if (Files.exists(certsDir.resolve("ca.pem"))) {
                    Files.copy(certsDir.resolve("ca.pem"), prevDir.resolve("ca.pem"));
                }
            }

            // Move staged -> active (atomic swap via .wip)
            writeAtomic(certsDir.resolve("cert.pem"), Files.readAllBytes(stagedDir.resolve("cert.pem")));
            writeAtomicRestricted(certsDir.resolve("key.pem"), Files.readAllBytes(stagedDir.resolve("key.pem")));
            if (Files.exists(stagedDir.resolve("ca.pem"))) {
                writeAtomic(certsDir.resolve("ca.pem"), Files.readAllBytes(stagedDir.resolve("ca.pem")));
            }

            // Clean up staged
            deleteDirectory(stagedDir);
            log.info("Certificate activated successfully");
        } catch (IOException e) {
            throw new RuntimeException("Failed to activate certificate", e);
        }
    }

    @Override
    public void restore() {
        try {
            Path prevDir = certsDir.resolve("prev");
            if (!Files.exists(prevDir.resolve("cert.pem"))) {
                throw new IllegalStateException("No archived certificate to restore");
            }

            // Swap: active <-> prev using a temp dir
            Path tempDir = certsDir.resolve("swap-tmp");
            Files.createDirectories(tempDir);

            // Current active -> temp
            moveFile(certsDir.resolve("cert.pem"), tempDir.resolve("cert.pem"));
            moveFile(certsDir.resolve("key.pem"), tempDir.resolve("key.pem"));
            if (Files.exists(certsDir.resolve("ca.pem"))) {
                moveFile(certsDir.resolve("ca.pem"), tempDir.resolve("ca.pem"));
            }

            // Prev -> active
            writeAtomic(certsDir.resolve("cert.pem"), Files.readAllBytes(prevDir.resolve("cert.pem")));
            writeAtomicRestricted(certsDir.resolve("key.pem"), Files.readAllBytes(prevDir.resolve("key.pem")));
            if (Files.exists(prevDir.resolve("ca.pem"))) {
                writeAtomic(certsDir.resolve("ca.pem"), Files.readAllBytes(prevDir.resolve("ca.pem")));
            } else {
                Files.deleteIfExists(certsDir.resolve("ca.pem"));
            }

            // Temp -> prev
            deleteDirectory(prevDir);
            Files.createDirectories(prevDir);
            moveFile(tempDir.resolve("cert.pem"), prevDir.resolve("cert.pem"));
            moveFile(tempDir.resolve("key.pem"), prevDir.resolve("key.pem"));
            if (Files.exists(tempDir.resolve("ca.pem"))) {
                moveFile(tempDir.resolve("ca.pem"), prevDir.resolve("ca.pem"));
            }

            deleteDirectory(tempDir);
            log.info("Certificate restored from archive");
        } catch (IOException e) {
            throw new RuntimeException("Failed to restore certificate", e);
        }
    }

    @Override
    public void discardStaged() {
        try {
            deleteDirectory(certsDir.resolve("staged"));
            log.info("Staged certificate discarded");
        } catch (IOException e) {
            throw new RuntimeException("Failed to discard staged certificate", e);
        }
    }

    @Override
    public void generateSelfSigned(String hostname) {
        try {
            // Use keytool to generate a self-signed cert, then export to PEM
            // This is a fallback — the init container normally handles this
            log.warn("generateSelfSigned called on DockerCertificateManager — " +
                     "this is typically handled by the traefik-certs init container");
        } catch (Exception e) {
            throw new RuntimeException("Failed to generate self-signed certificate", e);
        }
    }

    @Override
    public byte[] getCaBundle() {
        try {
            Path caPath = certsDir.resolve("ca.pem");
            return Files.exists(caPath) ? Files.readAllBytes(caPath) : null;
        } catch (IOException e) {
            log.warn("Failed to read CA bundle: {}", e.getMessage());
            return null;
        }
    }

    // --- Internal helpers ---

    private CertificateInfo readCertInfo(Path certPath, Path caPath) {
        if (!Files.exists(certPath)) return null;
        try {
            byte[] certBytes = Files.readAllBytes(certPath);
            X509Certificate cert = parseCertificate(certBytes);
            boolean hasCa = Files.exists(caPath);
            boolean selfSigned = cert.getIssuerX500Principal().equals(cert.getSubjectX500Principal());
            return toCertInfo(cert, hasCa, selfSigned);
        } catch (Exception e) {
            log.warn("Failed to read cert info from {}: {}", certPath, e.getMessage());
            return null;
        }
    }

    private CertificateInfo toCertInfo(X509Certificate cert, boolean hasCa, boolean selfSigned) {
        try {
            String fingerprint = HexFormat.ofDelimiter(":").formatHex(
                MessageDigest.getInstance("SHA-256").digest(cert.getEncoded()));
            // Auto-detect self-signed
            if (cert.getIssuerX500Principal().equals(cert.getSubjectX500Principal())) {
                selfSigned = true;
            }
            return new CertificateInfo(
                cert.getSubjectX500Principal().getName(),
                cert.getIssuerX500Principal().getName(),
                cert.getNotBefore().toInstant(),
                cert.getNotAfter().toInstant(),
                hasCa,
                selfSigned,
                fingerprint
            );
        } catch (Exception e) {
            throw new RuntimeException("Failed to extract cert info", e);
        }
    }

    static X509Certificate parseCertificate(byte[] pem) throws Exception {
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(pem));
    }

    static List<X509Certificate> parseCertificates(byte[] pem) throws Exception {
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        var certs = cf.generateCertificates(new ByteArrayInputStream(pem));
        return certs.stream().map(c -> (X509Certificate) c).toList();
    }

    static boolean isEncryptedKey(byte[] pem) {
        String s = new String(pem);
        return s.contains("ENCRYPTED PRIVATE KEY");
    }

    static java.security.PrivateKey parsePrivateKey(byte[] pem, String password) throws Exception {
        String pemStr = new String(pem);

        // Encrypted PKCS#8 key
        if (pemStr.contains("BEGIN ENCRYPTED PRIVATE KEY")) {
            if (password == null || password.isEmpty()) {
                throw new IllegalArgumentException("Private key is encrypted but no password was provided");
            }
            var matcher = java.util.regex.Pattern
                .compile("-----BEGIN ENCRYPTED PRIVATE KEY-----(.+?)-----END ENCRYPTED PRIVATE KEY-----",
                         java.util.regex.Pattern.DOTALL)
                .matcher(pemStr);
            if (!matcher.find()) {
                throw new IllegalArgumentException("Malformed encrypted private key PEM");
            }
            String base64 = matcher.group(1).replaceAll("\\s+", "");
            byte[] decoded = Base64.getDecoder().decode(base64);

            var encryptedInfo = new javax.crypto.EncryptedPrivateKeyInfo(decoded);
            var pbeKeySpec = new javax.crypto.spec.PBEKeySpec(password.toCharArray());
            var keyFactory = javax.crypto.SecretKeyFactory.getInstance(encryptedInfo.getAlgName());
            var secretKey = keyFactory.generateSecret(pbeKeySpec);

            var cipher = javax.crypto.Cipher.getInstance(encryptedInfo.getAlgName());
            cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, encryptedInfo.getAlgParameters());

            var pkcs8Spec = encryptedInfo.getKeySpec(cipher);
            return KeyFactory.getInstance("RSA").generatePrivate(pkcs8Spec);
        }

        // Unencrypted key — extract base64 between PEM markers (handles Bag Attributes etc.)
        var matcher = java.util.regex.Pattern
            .compile("-----BEGIN (?:RSA )?PRIVATE KEY-----(.+?)-----END (?:RSA )?PRIVATE KEY-----",
                     java.util.regex.Pattern.DOTALL)
            .matcher(pemStr);
        if (!matcher.find()) {
            throw new IllegalArgumentException("No private key PEM block found");
        }
        String base64 = matcher.group(1).replaceAll("\\s+", "");
        byte[] decoded = Base64.getDecoder().decode(base64);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);
        return KeyFactory.getInstance("RSA").generatePrivate(spec);
    }

    static byte[] encodePrivateKeyPem(java.security.PrivateKey key) {
        var sb = new StringBuilder();
        sb.append("-----BEGIN PRIVATE KEY-----\n");
        sb.append(Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString(key.getEncoded()));
        sb.append("\n-----END PRIVATE KEY-----\n");
        return sb.toString().getBytes();
    }

    private void writeAtomic(Path target, byte[] data) throws IOException {
        Path wip = target.resolveSibling(target.getFileName() + ".wip");
        Files.write(wip, data);
        Files.move(wip, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
    }

    /** Write with owner-only permissions (0600) for private key files. */
    private void writeAtomicRestricted(Path target, byte[] data) throws IOException {
        writeAtomic(target, data);
        try {
            var posix = Files.getFileAttributeView(target, java.nio.file.attribute.PosixFileAttributeView.class);
            if (posix != null) {
                posix.setPermissions(java.util.Set.of(
                    java.nio.file.attribute.PosixFilePermission.OWNER_READ,
                    java.nio.file.attribute.PosixFilePermission.OWNER_WRITE
                ));
            }
        } catch (UnsupportedOperationException e) {
            // Non-POSIX filesystem (e.g., Windows) — skip
            log.debug("Cannot set POSIX permissions on {}: {}", target, e.getMessage());
        }
    }

    private void moveFile(Path source, Path target) throws IOException {
        Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
    }

    private void deleteDirectory(Path dir) throws IOException {
        if (!Files.exists(dir)) return;
        try (var walk = Files.walk(dir)) {
            walk.sorted(java.util.Comparator.reverseOrder())
                .forEach(p -> {
                    try { Files.deleteIfExists(p); } catch (IOException ignored) {}
                });
        }
    }
}
