import "dotenv/config"
import { Pool, type PoolClient } from "pg"

import { loadSecrets, secrets } from "../src/utils/secrets"

type Args = {
    tenantId?: number
    fromNumber?: string
    toNumber?: string
    fromId?: number
    toId?: number
    types: string[]
    execute: boolean
    unlinkRelated: boolean
}

type TargetDocument = {
    id: number
    documentNumber: string | null
    type: string
    title: string | null
    state: string
}

const DEFAULT_TYPES = ["invoices"]

function printUsage() {
    console.log(`
Löscht einen Bereich von Ausgangsbelegen und deren direkt verknüpfte DB-Dokumente.

Standardmäßig läuft das Skript als Dry-Run. Erst --execute löscht wirklich.

Beispiele:
  npm run invoices:delete-range -- --tenant 41 --from RE-2026-001 --to RE-2026-010
  npm run invoices:delete-range -- --tenant 41 --from-id 1200 --to-id 1210 --execute
  npm run invoices:delete-range -- --tenant 41 --from RE-2026-001 --to RE-2026-010 --type invoices --type advanceInvoices --execute

Optionen:
  --tenant <id>              Pflicht: Tenant-ID
  --from <nummer>            Erste Rechnungsnummer im Bereich
  --to <nummer>              Letzte Rechnungsnummer im Bereich
  --from-id <id>             Erste Beleg-ID im Bereich
  --to-id <id>               Letzte Beleg-ID im Bereich
  --type <type>              Belegtyp, mehrfach möglich. Standard: invoices
  --execute                  Wirklich löschen. Ohne diese Option nur Vorschau.
  --unlink-related           Externe Belege, die auf Zielbelege zeigen, werden entkoppelt.
  --help                     Hilfe anzeigen
`)
}

function readValue(args: string[], index: number, flag: string) {
    const value = args[index + 1]
    if (!value || value.startsWith("--")) {
        throw new Error(`Für ${flag} fehlt ein Wert.`)
    }
    return value
}

function readNumber(args: string[], index: number, flag: string) {
    const value = Number(readValue(args, index, flag))
    if (!Number.isSafeInteger(value) || value <= 0) {
        throw new Error(`${flag} muss eine positive Ganzzahl sein.`)
    }
    return value
}

function parseArgs(argv: string[]): Args {
    const parsed: Args = {
        types: [],
        execute: false,
        unlinkRelated: false,
    }

    for (let i = 0; i < argv.length; i += 1) {
        const arg = argv[i]

        if (arg === "--help" || arg === "-h") {
            printUsage()
            process.exit(0)
        }

        if (arg === "--tenant") {
            parsed.tenantId = readNumber(argv, i, arg)
            i += 1
            continue
        }

        if (arg === "--from") {
            parsed.fromNumber = readValue(argv, i, arg)
            i += 1
            continue
        }

        if (arg === "--to") {
            parsed.toNumber = readValue(argv, i, arg)
            i += 1
            continue
        }

        if (arg === "--from-id") {
            parsed.fromId = readNumber(argv, i, arg)
            i += 1
            continue
        }

        if (arg === "--to-id") {
            parsed.toId = readNumber(argv, i, arg)
            i += 1
            continue
        }

        if (arg === "--type") {
            parsed.types.push(readValue(argv, i, arg))
            i += 1
            continue
        }

        if (arg === "--execute") {
            parsed.execute = true
            continue
        }

        if (arg === "--unlink-related") {
            parsed.unlinkRelated = true
            continue
        }

        throw new Error(`Unbekannte Option: ${arg}`)
    }

    if (!parsed.tenantId) throw new Error("--tenant ist Pflicht.")

    const hasNumberRange = parsed.fromNumber !== undefined || parsed.toNumber !== undefined
    const hasIdRange = parsed.fromId !== undefined || parsed.toId !== undefined

    if (hasNumberRange && hasIdRange) {
        throw new Error("Bitte entweder Rechnungsnummernbereich oder ID-Bereich verwenden, nicht beides.")
    }

    if (hasNumberRange && (!parsed.fromNumber || !parsed.toNumber)) {
        throw new Error("Für einen Rechnungsnummernbereich sind --from und --to Pflicht.")
    }

    if (hasIdRange && (!parsed.fromId || !parsed.toId)) {
        throw new Error("Für einen ID-Bereich sind --from-id und --to-id Pflicht.")
    }

    if (!hasNumberRange && !hasIdRange) {
        throw new Error("Bitte einen Bereich mit --from/--to oder --from-id/--to-id angeben.")
    }

    if (parsed.fromNumber && parsed.toNumber && parsed.fromNumber > parsed.toNumber) {
        throw new Error("--from darf bei Textvergleich nicht größer als --to sein.")
    }

    if (parsed.fromId && parsed.toId && parsed.fromId > parsed.toId) {
        throw new Error("--from-id darf nicht größer als --to-id sein.")
    }

    if (parsed.types.length === 0) parsed.types = DEFAULT_TYPES

    return parsed
}

async function loadConnectionString() {
    if (process.env.DATABASE_URL) return process.env.DATABASE_URL

    await loadSecrets()
    if (secrets.DATABASE_URL) return secrets.DATABASE_URL

    throw new Error("DATABASE_URL ist nicht konfiguriert.")
}

function buildTargetWhere(args: Args, startParam = 1) {
    const values: unknown[] = [args.tenantId, args.types]
    const clauses = [
        `"tenant" = $${startParam}`,
        `"type" = ANY($${startParam + 1}::text[])`,
    ]

    if (args.fromNumber && args.toNumber) {
        values.push(args.fromNumber, args.toNumber)
        clauses.push(`"documentNumber" >= $${startParam + 2}`)
        clauses.push(`"documentNumber" <= $${startParam + 3}`)
    } else {
        values.push(args.fromId, args.toId)
        clauses.push(`"id" >= $${startParam + 2}`)
        clauses.push(`"id" <= $${startParam + 3}`)
    }

    return {
        values,
        whereSql: clauses.join(" AND "),
    }
}

async function selectTargetDocuments(client: PoolClient, args: Args) {
    const target = buildTargetWhere(args)
    const result = await client.query<TargetDocument>(
        `
            SELECT "id", "documentNumber", "type", "title", "state"
            FROM "createddocuments"
            WHERE ${target.whereSql}
            ORDER BY "id"
        `,
        target.values
    )

    return result.rows
}

async function countByTarget(client: PoolClient, table: string, column: string, targetIds: number[]) {
    const result = await client.query<{ count: string }>(
        `SELECT COUNT(*)::int AS "count" FROM "${table}" WHERE "${column}" = ANY($1::bigint[])`,
        [targetIds]
    )

    return Number(result.rows[0]?.count || 0)
}

async function findExternalLinkedDocuments(client: PoolClient, targetIds: number[]) {
    const result = await client.query<TargetDocument>(
        `
            SELECT "id", "documentNumber", "type", "title", "state"
            FROM "createddocuments"
            WHERE "linkedDocument" = ANY($1::bigint[])
              AND NOT ("id" = ANY($1::bigint[]))
            ORDER BY "id"
        `,
        [targetIds]
    )

    return result.rows
}

async function deleteByTarget(client: PoolClient, table: string, column: string, targetIds: number[]) {
    const result = await client.query(
        `DELETE FROM "${table}" WHERE "${column}" = ANY($1::bigint[])`,
        [targetIds]
    )

    return result.rowCount || 0
}

function printTargets(targets: TargetDocument[]) {
    console.table(
        targets.map((item) => ({
            id: item.id,
            nummer: item.documentNumber,
            typ: item.type,
            status: item.state,
            titel: item.title,
        }))
    )
}

async function run() {
    const args = parseArgs(process.argv.slice(2))
    const connectionString = await loadConnectionString()
    const pool = new Pool({ connectionString, max: 1 })
    const client = await pool.connect()

    try {
        await client.query("BEGIN")

        const targets = await selectTargetDocuments(client, args)
        if (targets.length === 0) {
            console.log("Keine passenden Rechnungen gefunden.")
            await client.query("ROLLBACK")
            return
        }

        const targetIds = targets.map((item) => item.id)
        const [fileCount, historyCount, allocationCount, externalLinks] = await Promise.all([
            countByTarget(client, "files", "createddocument", targetIds),
            countByTarget(client, "historyitems", "createddocument", targetIds),
            countByTarget(client, "statementallocations", "cd_id", targetIds),
            findExternalLinkedDocuments(client, targetIds),
        ])

        console.log(`Tenant: ${args.tenantId}`)
        console.log(`Belegtypen: ${args.types.join(", ")}`)
        console.log(`Modus: ${args.execute ? "Löschen" : "Dry-Run"}`)
        console.log("")
        printTargets(targets)
        console.log("")
        console.log("Betroffene verknüpfte DB-Zeilen:")
        console.log(`- Dateien: ${fileCount}`)
        console.log(`- Historie: ${historyCount}`)
        console.log(`- Bank-Zuordnungen: ${allocationCount}`)

        if (externalLinks.length > 0 && !args.unlinkRelated) {
            console.log("")
            console.log("Abbruch: Andere Belege verweisen auf Zielbelege.")
            console.log("Nutze --unlink-related, wenn diese Verknüpfungen vor dem Löschen entfernt werden sollen.")
            printTargets(externalLinks)
            await client.query("ROLLBACK")
            process.exitCode = 1
            return
        }

        if (!args.execute) {
            console.log("")
            console.log("Dry-Run abgeschlossen. Es wurde nichts gelöscht. Zum Löschen --execute ergänzen.")
            await client.query("ROLLBACK")
            return
        }

        if (externalLinks.length > 0) {
            await client.query(
                `
                    UPDATE "createddocuments"
                    SET "linkedDocument" = NULL
                    WHERE "linkedDocument" = ANY($1::bigint[])
                      AND NOT ("id" = ANY($1::bigint[]))
                `,
                [targetIds]
            )
        }

        await client.query(
            `UPDATE "createddocuments" SET "linkedDocument" = NULL WHERE "id" = ANY($1::bigint[])`,
            [targetIds]
        )

        const deletedFiles = await deleteByTarget(client, "files", "createddocument", targetIds)
        const deletedHistory = await deleteByTarget(client, "historyitems", "createddocument", targetIds)
        const deletedAllocations = await deleteByTarget(client, "statementallocations", "cd_id", targetIds)

        const deletedDocuments = await client.query(
            `DELETE FROM "createddocuments" WHERE "id" = ANY($1::bigint[])`,
            [targetIds]
        )

        await client.query("COMMIT")

        console.log("")
        console.log("Löschung abgeschlossen:")
        console.log(`- Rechnungen: ${deletedDocuments.rowCount || 0}`)
        console.log(`- Dateien: ${deletedFiles}`)
        console.log(`- Historie: ${deletedHistory}`)
        console.log(`- Bank-Zuordnungen: ${deletedAllocations}`)
        if (externalLinks.length > 0) {
            console.log(`- Entkoppelte externe Belege: ${externalLinks.length}`)
        }
    } catch (error) {
        await client.query("ROLLBACK")
        throw error
    } finally {
        client.release()
        await pool.end()
    }
}

run().catch((error) => {
    console.error("Fehler beim Löschen der Rechnungen:")
    console.error(error)
    process.exit(1)
})
