import {
    decodePDFRawStream,
    PDFDict,
    PDFDocument,
    PDFHexString,
    PDFName,
    PDFRawStream,
    PDFString,
} from "pdf-lib"

export type ElectronicInvoiceSyntax = "cii" | "ubl-invoice" | "ubl-credit-note"

export type ElectronicInvoiceDetection = {
    container: "pdf" | "xml"
    syntax: ElectronicInvoiceSyntax
    xml: Buffer
    attachmentName: string | null
}

type FileLike = {
    name?: string | null
    path?: string | null
    mimeType?: string | null
}

const decodePdfText = (value: unknown): string | null => {
    if (value instanceof PDFString || value instanceof PDFHexString) {
        return value.decodeText()
    }

    return null
}

const detectXmlSyntax = (xml: Buffer): ElectronicInvoiceSyntax | null => {
    const sample = xml.subarray(0, Math.min(xml.length, 64 * 1024)).toString("utf8")

    if (/<(?:[A-Za-z_][\w.-]*:)?CrossIndustryInvoice(?:\s|>)/i.test(sample)) {
        return "cii"
    }

    if (
        /<(?:[A-Za-z_][\w.-]*:)?Invoice(?:\s|>)/i.test(sample)
        && /urn:oasis:names:specification:ubl:schema:xsd:Invoice-2/i.test(sample)
    ) {
        return "ubl-invoice"
    }

    if (
        /<(?:[A-Za-z_][\w.-]*:)?CreditNote(?:\s|>)/i.test(sample)
        && /urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2/i.test(sample)
    ) {
        return "ubl-credit-note"
    }

    return null
}

const extractXmlFromFileSpec = (fileSpec: PDFDict): { name: string | null, xml: Buffer } | null => {
    const context = fileSpec.context
    const embeddedFiles = context.lookupMaybe(fileSpec.get(PDFName.of("EF")), PDFDict)

    if (!embeddedFiles) return null

    const stream = context.lookup(
        embeddedFiles.get(PDFName.of("UF")) || embeddedFiles.get(PDFName.of("F")),
    )

    if (!(stream instanceof PDFRawStream)) return null

    const name = decodePdfText(
        context.lookup(fileSpec.get(PDFName.of("UF")) || fileSpec.get(PDFName.of("F"))),
    )
    const contents = Buffer.from(decodePDFRawStream(stream).decode())

    return { name, xml: contents }
}

const detectEmbeddedInvoice = async (pdf: Buffer): Promise<ElectronicInvoiceDetection | null> => {
    let document: PDFDocument

    try {
        document = await PDFDocument.load(pdf, {
            ignoreEncryption: true,
            updateMetadata: false,
        })
    } catch {
        return null
    }

    for (const [, object] of document.context.enumerateIndirectObjects()) {
        if (!(object instanceof PDFDict)) continue

        const type = object.get(PDFName.of("Type"))
        if (!(type instanceof PDFName) || type.asString() !== "/Filespec") continue

        const embedded = extractXmlFromFileSpec(object)
        if (!embedded) continue

        const syntax = detectXmlSyntax(embedded.xml)
        if (!syntax) continue

        return {
            container: "pdf",
            syntax,
            xml: embedded.xml,
            attachmentName: embedded.name,
        }
    }

    return null
}

const hasPdfFileType = (file: FileLike, data: Buffer) => {
    const filename = String(file.name || file.path || "").toLowerCase()
    const mimeType = String(file.mimeType || "").toLowerCase().split(";")[0].trim()

    return filename.endsWith(".pdf") || mimeType === "application/pdf" || data.subarray(0, 5).toString("ascii") === "%PDF-"
}

export const detectElectronicInvoice = async (
    data: Buffer,
    file: FileLike,
): Promise<ElectronicInvoiceDetection | null> => {
    if (hasPdfFileType(file, data)) {
        return detectEmbeddedInvoice(data)
    }

    const beginsLikeXml = data.subarray(0, 1024).toString("utf8").replace(/^\uFEFF/, "").trimStart().startsWith("<")
    const syntax = beginsLikeXml ? detectXmlSyntax(data) : null

    if (syntax) {
        return {
            container: "xml",
            syntax,
            xml: data,
            attachmentName: file.name || file.path?.split("/").pop() || null,
        }
    }

    return null
}
