import * as openpgp from "openpgp";
import { base45Encode, base45Decode } from "./base45";
import { crc16CcittFalse } from "./crc16";
import { encryptToKrux, decryptFromKrux } from "./krux";
import { decodeSeedQR } from './seedqr';
import type {
    SeedPgpPlaintext,
    ParsedSeedPgpFrame,
    EncryptionMode,
    EncryptionParams,
    DecryptionParams,
    EncryptionResult
} from "./types";

// Configure OpenPGP.js (disable warnings)
openpgp.config.showComment = false;
openpgp.config.showVersion = false;

function nonEmptyTrimmed(s?: string): string | undefined {
    if (!s) return undefined;
    const t = s.trim();
    return t.length ? t : undefined;
}

export function normalizeMnemonic(words: string): string {
    return words.trim().toLowerCase().replace(/\s+/g, " ");
}

export function buildPlaintext(
    mnemonic: string,
    bip39PassphraseUsed: boolean,
    recipientFingerprints?: string[]
): SeedPgpPlaintext {
    const plain: SeedPgpPlaintext = {
        v: 1,
        t: "bip39",
        w: normalizeMnemonic(mnemonic),
        l: "en",
        pp: bip39PassphraseUsed ? 1 : 0,
    };
    if (recipientFingerprints && recipientFingerprints.length > 0) {
        plain.fpr = recipientFingerprints;
    }
    return plain;
}

export function frameEncode(pgpBinary: Uint8Array): string {
    const crc = crc16CcittFalse(pgpBinary);
    const b45 = base45Encode(pgpBinary);
    return `SEEDPGP1:0:${crc}:${b45}`;
}

/**
 * Validates a PGP public key for encryption use.
 * Checks: encryption capability, expiration, key strength, and self-signatures.
 */
export async function validatePGPKey(armoredKey: string): Promise<{
    valid: boolean;
    error?: string;
    fingerprint?: string;
    keySize?: number;
    expirationDate?: Date;
}> {
    try {
        const key = await openpgp.readKey({ armoredKey });

        // 1. Verify encryption capability
        try {
            await key.getEncryptionKey();
        } catch {
            return { valid: false, error: "Key has no usable encryption subkey" };
        }

        // 2. Check key expiration
        const expirationTime = await key.getExpirationTime();
        if (expirationTime && expirationTime < new Date()) {
            return { valid: false, error: "PGP key has expired" };
        }

        // 3. Check key strength (if available)
        let keySize = 0;
        try {
            const mainKey = key as any;
            if (mainKey.getBitSize) {
                keySize = mainKey.getBitSize();
                if (keySize > 0 && keySize < 2048) {
                    return { valid: false, error: `PGP key too small (${keySize} bits). Minimum 2048.` };
                }
            }
        } catch (e) {
            // Unable to determine key size, but continue
        }

        // 4. Verify primary key (at least check it exists)
        try {
            await key.verifyPrimaryKey();
            // Note: openpgp.js may not have all verification methods in all versions
            // We proceed even if verification is not fully available
        } catch (e) {
            // Verification not available or failed, but key is still usable
        }

        return {
            valid: true,
            fingerprint: key.getFingerprint().toUpperCase(),
            keySize: keySize || undefined,
            expirationDate: expirationTime instanceof Date ? expirationTime : undefined,
        };
    } catch (e) {
        return {
            valid: false,
            error: `Failed to parse PGP key: ${e instanceof Error ? e.message : 'Unknown error'}`
        };
    }
}

export function frameParse(text: string): ParsedSeedPgpFrame {
    const s = text.trim().replace(/^["']|["']$/g, "").replace(/[\n\r\t]/g, "");
    if (s.startsWith("SEEDPGP1:")) {
        const parts = s.split(":");
        if (parts.length < 4) {
            throw new Error("Invalid frame format (need at least 4 colon-separated parts)");
        }
        const prefix = parts[0];
        const frame = parts[1];
        const crc16 = parts[2].toUpperCase();
        const b45 = parts.slice(3).join(":");

        if (prefix !== "SEEDPGP1") throw new Error("Invalid prefix");
        if (frame !== "0") throw new Error("Multipart frames not supported in this prototype");
        if (!/^[0-9A-F]{4}$/.test(crc16)) throw new Error("Invalid CRC16 format (must be 4 hex chars)");

        return { kind: "single", crc16, b45 };
    } else {
        // It's not a full frame. Assume the ENTIRE string is the base45 payload.
        // We will have to skip the CRC check.
        return { kind: "single", crc16: "0000", b45: s, rawPayload: true };
    }
}

export function frameDecodeToPgpBytes(frameText: string): Uint8Array {
    const f = frameParse(frameText);
    const pgp = base45Decode(f.b45);

    // If it's a raw payload, we cannot and do not verify the CRC.
    if (f.rawPayload) {
        return pgp;
    }

    const crc = crc16CcittFalse(pgp);
    if (crc !== f.crc16) {
        throw new Error(`CRC16 mismatch! Expected: ${f.crc16}, Got: ${crc}. QR scan may be corrupted.`);
    }
    return pgp;
}

export async function encryptToSeedPgp(params: {
    plaintext: SeedPgpPlaintext;
    publicKeyArmored?: string;
    messagePassword?: string;
}): Promise<{ framed: string; pgpBytes: Uint8Array; recipientFingerprint?: string }> {
    const pub = nonEmptyTrimmed(params.publicKeyArmored);
    const pw = nonEmptyTrimmed(params.messagePassword);

    if (!pub && !pw) {
        throw new Error("Provide either a PGP public key or a message password (or both).");
    }

    let encryptionKeys: openpgp.PublicKey[] = [];
    let recipientFingerprint: string | undefined;

    if (pub) {
        const pubKey = await openpgp.readKey({ armoredKey: pub });
        try {
            await pubKey.getEncryptionKey();
        } catch {
            throw new Error("This public key has no usable encryption subkey (E).");
        }

        recipientFingerprint = pubKey.getFingerprint().toUpperCase();
        encryptionKeys = [pubKey];
    }

    const message = await openpgp.createMessage({ text: JSON.stringify(params.plaintext) });

    const encrypted = await openpgp.encrypt({
        message,
        encryptionKeys,
        passwords: pw ? [pw] : [],
        format: "binary",
        config: {
            preferredSymmetricAlgorithm: openpgp.enums.symmetric.aes256,
        },
    });

    const pgpBytes = new Uint8Array(encrypted as Uint8Array);
    return { framed: frameEncode(pgpBytes), pgpBytes, recipientFingerprint };
}

export async function decryptSeedPgp(params: {
    frameText: string;
    privateKeyArmored?: string;
    privateKeyPassphrase?: string;
    messagePassword?: string;
}): Promise<SeedPgpPlaintext> {
    const pgpBytes = frameDecodeToPgpBytes(params.frameText);
    const message = await openpgp.readMessage({ binaryMessage: pgpBytes });
    const encKeyIds: openpgp.KeyID[] = message.getEncryptionKeyIDs?.() ?? [];

    const privArmored = nonEmptyTrimmed(params.privateKeyArmored);
    const privPw = nonEmptyTrimmed(params.privateKeyPassphrase);
    const msgPw = nonEmptyTrimmed(params.messagePassword);

    let decryptionKeys: openpgp.PrivateKey[] = [];

    if (privArmored) {
        let privKey = await openpgp.readPrivateKey({ armoredKey: privArmored });

        if (!privKey.isDecrypted()) {
            if (!privPw) {
                throw new Error("Private key is still locked. Enter the private key passphrase.");
            }
            privKey = await openpgp.decryptKey({ privateKey: privKey, passphrase: privPw });
            if (!privKey.isDecrypted()) {
                throw new Error("Private key passphrase incorrect (key still locked).");
            }
        }

        // Preflight: validate the private key matches a recipient
        if (encKeyIds.length) {
            const dec = await privKey.getDecryptionKeys();
            const decArr = Array.isArray(dec) ? dec : [dec];
            const decKeyIds = decArr.map((k: any) => k.getKeyID().toHex().toUpperCase());
            const want = new Set(encKeyIds.map((kid: openpgp.KeyID) => kid.toHex().toUpperCase()));
            const matched = decKeyIds.some((id: string) => want.has(id));

            if (!matched) {
                throw new Error(
                    "This payload is not encrypted to the provided private key (no matching recipient KeyID)."
                );
            }
        }

        decryptionKeys = [privKey];
    }

    let decryptResult;
    try {
        const decryptOptions: any = {
            message,
            format: "utf8",
        };

        if (decryptionKeys.length > 0) {
            decryptOptions.decryptionKeys = decryptionKeys;
        }

        if (msgPw) {
            decryptOptions.passwords = [msgPw];
        }

        decryptResult = await openpgp.decrypt(decryptOptions);
    } catch (err: any) {
        console.error("SeedPGP: decrypt failed:", err.message);
        throw err;
    }

    const { data } = decryptResult;
    const obj = JSON.parse(data as string) as SeedPgpPlaintext;

    if (obj.v !== 1) throw new Error(`Unsupported version: ${obj.v}`);
    if (obj.t !== "bip39") throw new Error(`Unsupported type: ${obj.t}`);
    if (obj.l !== "en") throw new Error(`Unsupported language: ${obj.l}`);

    return obj;
}

/**
 * Unified encryption function supporting both PGP and Krux modes
 */
export async function encryptToSeed(params: EncryptionParams): Promise<EncryptionResult> {
    const mode = params.mode || 'pgp';

    if (mode === 'krux') {
        const plaintextStr = typeof params.plaintext === 'string'
            ? params.plaintext
            : params.plaintext.w;

        const passphrase = params.messagePassword || '';
        if (!passphrase) {
            throw new Error("Krux mode requires a message password (passphrase)");
        }

        try {
            const result = await encryptToKrux({
                mnemonic: plaintextStr,
                passphrase: passphrase
            });

            return {
                framed: result.kefBase43,
                label: result.label,
                version: result.version,
                iterations: result.iterations,
            };
        } catch (error) {
            if (error instanceof Error) {
                throw new Error(`Krux encryption failed: ${error.message}`);
            }
            throw error;
        }
    }

    // Default to PGP mode
    const plaintextObj = typeof params.plaintext === 'string'
        ? buildPlaintext(params.plaintext, false)
        : params.plaintext;

    const result = await encryptToSeedPgp({
        plaintext: plaintextObj,
        publicKeyArmored: params.publicKeyArmored,
        messagePassword: params.messagePassword,
    });

    return {
        framed: result.framed,
        pgpBytes: result.pgpBytes,
        recipientFingerprint: result.recipientFingerprint,
    };
}

/**
 * Unified decryption function supporting both PGP and Krux modes
 */
export async function decryptFromSeed(params: DecryptionParams): Promise<SeedPgpPlaintext> {
    const mode = params.mode || 'pgp';

    if (mode === 'krux') {
        const passphrase = params.messagePassword || '';
        if (!passphrase) {
            throw new Error("Krux mode requires a message password (passphrase)");
        }

        try {
            const result = await decryptFromKrux({
                kefData: params.frameText,
                passphrase,
            });

            // Convert to SeedPgpPlaintext format for consistency
            return {
                v: 1,
                t: "bip39",
                w: result.mnemonic,
                l: "en",
                pp: 0,
            };
        } catch (error) {
            if (error instanceof Error) {
                throw new Error(`Krux decryption failed: ${error.message}`);
            }
            throw error;
        }
    }

    if (mode === 'seedqr') {
        try {
            const mnemonic = await decodeSeedQR(params.frameText);
            // Convert to SeedPgpPlaintext format for consistency
            return {
                v: 1,
                t: "bip39",
                w: mnemonic,
                l: "en",
                pp: 0,
            };
        } catch (error) {
            if (error instanceof Error) {
                throw new Error(`SeedQR decoding failed: ${error.message}`);
            }
            throw error;
        }
    }

    // Default to PGP mode
    return decryptSeedPgp({
        frameText: params.frameText,
        privateKeyArmored: params.privateKeyArmored,
        privateKeyPassphrase: params.privateKeyPassphrase,
        messagePassword: params.messagePassword,
    });
}

/**
 * Detect encryption mode from input text
 */
import { B43CHARS } from "./base43"; // Assuming B43CHARS is exported from base43.ts

// ... (other imports)

const BASE43_CHARS_ONLY_REGEX = new RegExp(`^[${B43CHARS.replace(/[\-\]\\]/g, '\\$&')}]+$`);

export function detectEncryptionMode(text: string): EncryptionMode {
    const trimmed = text.trim();

    // 1. Definite PGP
    if (trimmed.startsWith('SEEDPGP1:')) {
        return 'pgp';
    }

    // 2. Definite Krux KEF format
    if (trimmed.toUpperCase().startsWith('KEF:')) {
        return 'krux';
    }

    // 3. Standard SeedQR (all digits)
    if (/^\d+$/.test(trimmed) && trimmed.length >= 48) { // 12 words * 4 digits
        return 'seedqr';
    }

    // 4. Compact SeedQR (all hex)
    // 12 words = 16 bytes = 32 hex chars
    // 24 words = 32 bytes = 64 hex chars
    if (/^[0-9a-fA-F]+$/.test(trimmed) && (trimmed.length === 32 || trimmed.length === 64)) {
        return 'seedqr';
    }

    // 5. Krux Base43 format (uses a specific character set)
    if (BASE43_CHARS_ONLY_REGEX.test(trimmed)) {
        return 'krux';
    }

    // 6. Likely a plain text mnemonic (contains spaces)
    if (trimmed.includes(' ')) {
        return 'text';
    }

    // 7. Default for anything else
    return 'text';
}
