import * as fs from "node:fs"
import * as path from "node:path"
import { execFileSync } from "node:child_process"
import { and, eq } from "drizzle-orm"

import {
    authProfileBranches,
    authProfiles,
    authProfileTeams,
    branches,
    teams,
    tenants,
} from "../db/schema"

type ImportRow = {
    rowNumber: number
    mitarbeiter: string
    betrieb: string
    anstellung: string
    position: string
    bereich: string
    stundenMonat: number | null
    urlaub: number | null
}

type CliOptions = {
    workbookPath: string
    tenantId: number
    dryRun: boolean
    defaultBranchId: number | null
    branchMap: Record<string, number>
}

type ImportAction = "create" | "update"

function printHelp() {
    console.log(`
Importiert die Excel-Datei "Mitarbeiterliste.xlsm" in auth_profiles.

Verwendung:
  npm run profiles:import:mitarbeiterliste -- --tenant-id=12 --branch-map-file=./branch-map.json /pfad/zur/Mitarbeiterliste.xlsm

Optionen:
  --tenant-id=ID                Pflicht. Tenant-ID für den Import.
  --branch-map='{"Name":1}'     Optional. JSON-Mapping Betrieb -> branchId.
  --branch-map-file=DATEI       Optional. JSON-Datei mit Betrieb -> branchId.
  --default-branch-id=ID        Optional. Fallback-Branch-ID für nicht gemappte Betriebe.
  --dry-run                     Führt keine Schreiboperationen aus.
  --help                        Zeigt diese Hilfe an.

Beispiel branch-map.json:
{
  "Strandcafé": 10,
  "1848 Pütt": 11,
  "Oceans11": 12,
  "Winnys": 13
}
`.trim())
}

function parseArgs(argv: string[]): CliOptions | null {
    const options: CliOptions = {
        workbookPath: "/Users/florianfederspiel/Downloads/Mitarbeiterliste.xlsm",
        tenantId: Number.NaN,
        dryRun: false,
        defaultBranchId: null,
        branchMap: {},
    }

    for (const arg of argv) {
        if (arg === "--help" || arg === "-h") {
            return null
        }

        if (arg === "--dry-run") {
            options.dryRun = true
            continue
        }

        if (arg.startsWith("--tenant-id=")) {
            options.tenantId = Number(arg.slice("--tenant-id=".length))
            continue
        }

        if (arg.startsWith("--default-branch-id=")) {
            options.defaultBranchId = Number(arg.slice("--default-branch-id=".length))
            continue
        }

        if (arg.startsWith("--branch-map=")) {
            options.branchMap = parseBranchMap(arg.slice("--branch-map=".length), "CLI")
            continue
        }

        if (arg.startsWith("--branch-map-file=")) {
            const branchMapPath = path.resolve(arg.slice("--branch-map-file=".length))
            options.branchMap = parseBranchMap(fs.readFileSync(branchMapPath, "utf8"), branchMapPath)
            continue
        }

        if (!arg.startsWith("--")) {
            options.workbookPath = path.resolve(arg)
            continue
        }

        throw new Error(`Unbekanntes Argument: ${arg}`)
    }

    if (!Number.isFinite(options.tenantId)) {
        throw new Error("Bitte --tenant-id=... angeben.")
    }

    if (options.defaultBranchId != null && !Number.isFinite(options.defaultBranchId)) {
        throw new Error("--default-branch-id muss numerisch sein.")
    }

    return options
}

function parseBranchMap(raw: string, sourceLabel: string): Record<string, number> {
    let parsed: unknown
    try {
        parsed = JSON.parse(raw)
    } catch (error) {
        throw new Error(`Branch-Mapping aus ${sourceLabel} konnte nicht gelesen werden: ${(error as Error).message}`)
    }

    if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
        throw new Error(`Branch-Mapping aus ${sourceLabel} muss ein JSON-Objekt sein.`)
    }

    const normalizedEntries = Object.entries(parsed).map(([key, value]) => {
        const branchId = Number(value)
        if (!Number.isFinite(branchId)) {
            throw new Error(`Ungültige Branch-ID für "${key}" in ${sourceLabel}.`)
        }
        return [normalizeKey(key), branchId] as const
    })

    return Object.fromEntries(normalizedEntries)
}

function normalizeKey(value: string) {
    return String(value || "")
        .trim()
        .replace(/\s+/g, " ")
        .toLocaleLowerCase("de-DE")
}

function decodeXmlText(value: string) {
    return value
        .replace(/&amp;/g, "&")
        .replace(/&lt;/g, "<")
        .replace(/&gt;/g, ">")
        .replace(/&quot;/g, "\"")
        .replace(/&apos;/g, "'")
        .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
}

function getWorkbookXml(workbookPath: string, innerPath: string) {
    return execFileSync("unzip", ["-p", workbookPath, innerPath], {
        encoding: "utf8",
        stdio: ["ignore", "pipe", "pipe"],
    })
}

function readSharedStrings(workbookPath: string) {
    const xml = getWorkbookXml(workbookPath, "xl/sharedStrings.xml")
    return [...xml.matchAll(/<si\b[^>]*>([\s\S]*?)<\/si>/g)].map((match) => {
        const parts = [...match[1].matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/g)].map((part) => decodeXmlText(part[1]))
        return parts.join("")
    })
}

function readSheetRows(workbookPath: string, sharedStrings: string[]) {
    const sheetXml = getWorkbookXml(workbookPath, "xl/worksheets/sheet1.xml")
    const rows: Record<string, string>[] = []

    for (const rowMatch of sheetXml.matchAll(/<row\b[^>]*r="(\d+)"[^>]*>([\s\S]*?)<\/row>/g)) {
        const cellMap: Record<string, string> = {}
        const rowXml = rowMatch[2]

        for (const cellMatch of rowXml.matchAll(/<c\b([^>]*)>([\s\S]*?)<\/c>/g)) {
            const attrs = cellMatch[1]
            const cellXml = cellMatch[2]
            const refMatch = attrs.match(/r="([A-Z]+)\d+"/)
            if (!refMatch) continue

            const column = refMatch[1]
            const typeMatch = attrs.match(/t="([^"]+)"/)
            const type = typeMatch?.[1] || ""
            const valueMatch = cellXml.match(/<v>([\s\S]*?)<\/v>/)
            const inlineTextMatch = cellXml.match(/<t\b[^>]*>([\s\S]*?)<\/t>/)

            let value = ""
            if (type === "s" && valueMatch) {
                value = sharedStrings[Number(valueMatch[1])] || ""
            } else if (inlineTextMatch) {
                value = decodeXmlText(inlineTextMatch[1])
            } else if (valueMatch) {
                value = decodeXmlText(valueMatch[1])
            }

            cellMap[column] = value.trim()
        }

        rows.push(cellMap)
    }

    return rows
}

function parseNumber(value: string) {
    const normalized = String(value || "").trim().replace(",", ".")
    if (!normalized) return null

    const parsed = Number(normalized)
    return Number.isFinite(parsed) ? parsed : null
}

function parseWorkbook(workbookPath: string): ImportRow[] {
    const sharedStrings = readSharedStrings(workbookPath)
    const rows = readSheetRows(workbookPath, sharedStrings)

    if (!rows.length) {
        throw new Error("Die Arbeitsmappe enthält keine Zeilen.")
    }

    const header = rows[0]
    if (header.A !== "Mitarbeiter" || header.B !== "Betrieb") {
        throw new Error("Unerwartetes Format der Excel-Datei. Erwartet wurden die Spalten 'Mitarbeiter' und 'Betrieb'.")
    }

    return rows
        .slice(1)
        .map((row, index) => ({
            rowNumber: index + 2,
            mitarbeiter: row.A || "",
            betrieb: row.B || "",
            anstellung: row.C || "",
            position: row.D || "",
            bereich: row.E || "",
            stundenMonat: parseNumber(row.F || ""),
            urlaub: parseNumber(row.G || ""),
        }))
        .filter((row) => row.mitarbeiter)
}

function splitFullName(fullName: string) {
    const parts = fullName.trim().split(/\s+/).filter(Boolean)
    if (parts.length <= 1) {
        return {
            firstName: fullName.trim(),
            lastName: "Unbekannt",
        }
    }

    return {
        firstName: parts.slice(0, -1).join(" "),
        lastName: parts[parts.length - 1],
    }
}

function toWeeklyHours(monthlyHours: number | null) {
    if (monthlyHours == null) return null
    return Math.round(((monthlyHours * 12) / 52) * 100) / 100
}

function formatValue(value: unknown) {
    if (value == null) return "-"
    if (typeof value === "object") return JSON.stringify(value)
    return String(value)
}

function mapEmploymentCategory(value: string) {
    const normalized = normalizeKey(value)
    if (normalized === "aushilfe") return "Aushilfen"
    if (normalized === "teilzeit" || normalized === "vollzeit") return "Festangestellte"
    return null
}

function buildTeamName(bereich: string, anstellung: string) {
    const employmentCategory = mapEmploymentCategory(anstellung)
    const normalizedBereich = String(bereich || "").trim()
    if (!normalizedBereich || !employmentCategory) return null
    return `${normalizedBereich} ${employmentCategory}`
}

function collectFieldChanges(existing: any, nextPayload: Record<string, unknown>) {
    if (!existing) return []

    const changes: string[] = []
    for (const [field, nextValue] of Object.entries(nextPayload)) {
        const currentValue = existing[field]
        const currentFormatted = formatValue(currentValue)
        const nextFormatted = formatValue(nextValue)

        if (currentFormatted !== nextFormatted) {
            changes.push(`${field}: ${currentFormatted} -> ${nextFormatted}`)
        }
    }

    return changes
}

async function validateTenantAndBranches(
    db: any,
    tenantId: number,
    branchIds: number[]
) {
    const [tenant] = await db
        .select({ id: tenants.id, name: tenants.name })
        .from(tenants)
        .where(eq(tenants.id, tenantId))
        .limit(1)

    if (!tenant) {
        throw new Error(`Tenant ${tenantId} wurde nicht gefunden.`)
    }

    const branchRows = await db
        .select({ id: branches.id, name: branches.name })
        .from(branches)
        .where(eq(branches.tenant, tenantId))

    const validBranchIds = new Set(branchRows.map((branch: any) => Number(branch.id)))
    for (const branchId of branchIds) {
        if (!validBranchIds.has(branchId)) {
            throw new Error(`Branch-ID ${branchId} gehört nicht zum Tenant ${tenantId}.`)
        }
    }

    return {
        tenant,
        branchRows,
    }
}

async function loadTenantTeams(db: any, tenantId: number) {
    const teamRows = await db
        .select({
            id: teams.id,
            name: teams.name,
            branch: teams.branch,
            archived: teams.archived,
        })
        .from(teams)
        .where(eq(teams.tenant, tenantId))

    return new Map(
        teamRows
            .filter((team: any) => !team.archived)
            .map((team: any) => [`${team.branch}::${normalizeKey(team.name)}`, team])
    )
}

async function main() {
    const options = parseArgs(process.argv.slice(2))
    if (!options) {
        printHelp()
        return
    }

    if (!fs.existsSync(options.workbookPath)) {
        throw new Error(`Excel-Datei nicht gefunden: ${options.workbookPath}`)
    }

    const rows = parseWorkbook(options.workbookPath)
    if (!rows.length) {
        throw new Error("Keine importierbaren Mitarbeiter gefunden.")
    }

    const mappedBranchIds = [
        ...new Set(
            Object.values(options.branchMap)
                .concat(options.defaultBranchId != null ? [options.defaultBranchId] : [])
        ),
    ]

    const { db, pool } = await import("../db")

    try {
        const { tenant } = await validateTenantAndBranches(db, options.tenantId, mappedBranchIds)
        const teamByBranchAndName = await loadTenantTeams(db, options.tenantId)

        const existingProfiles = await db
            .select()
            .from(authProfiles)
            .where(eq(authProfiles.tenant_id, options.tenantId))

        const existingByName = new Map<string, any>()
        for (const profile of existingProfiles) {
            const key = normalizeKey(`${profile.first_name} ${profile.last_name}`)
            if (existingByName.has(key)) {
                throw new Error(`Mehrdeutiger bestehender Mitarbeiter: ${profile.first_name} ${profile.last_name}`)
            }
            existingByName.set(key, profile)
        }

        const duplicateImportNames = new Set<string>()
        const seenImportNames = new Set<string>()
        for (const row of rows) {
            const key = normalizeKey(row.mitarbeiter)
            if (seenImportNames.has(key)) duplicateImportNames.add(row.mitarbeiter)
            seenImportNames.add(key)
        }

        if (duplicateImportNames.size) {
            throw new Error(`Die Excel-Datei enthält doppelte Mitarbeiternamen: ${[...duplicateImportNames].join(", ")}`)
        }

        const missingBranchMappings = new Set<string>()
        const missingTeams = new Set<string>()
        const preparedRows = rows.map((row) => {
            const branchKey = normalizeKey(row.betrieb)
            const branchId = options.branchMap[branchKey] ?? options.defaultBranchId ?? null
            if (!branchId) {
                missingBranchMappings.add(row.betrieb || `(Zeile ${row.rowNumber})`)
            }

            const teamName = buildTeamName(row.bereich, row.anstellung)
            const team = branchId && teamName
                ? (teamByBranchAndName.get(`${branchId}::${normalizeKey(teamName)}`) as any) || null
                : null

            if (branchId && teamName && !team) {
                missingTeams.add(`${row.betrieb} | ${teamName}`)
            }

            return {
                ...row,
                branchId,
                teamId: team?.id ?? null,
                teamName,
                weeklyHours: toWeeklyHours(row.stundenMonat),
            }
        })

        if (missingBranchMappings.size) {
            throw new Error(
                `Für folgende Betriebe fehlt eine Branch-ID: ${[...missingBranchMappings].join(", ")}`
            )
        }

        if (missingTeams.size) {
            throw new Error(
                `Für folgende Niederlassung-/Bereich-Kombinationen fehlen Teams: ${[...missingTeams].join(", ")}`
            )
        }

        let createdProfiles = 0
        let updatedProfiles = 0
        const actionLogs: string[] = []

        for (const row of preparedRows) {
            const nameParts = splitFullName(row.mitarbeiter)
            const nameKey = normalizeKey(row.mitarbeiter)
            const existing = existingByName.get(nameKey)

            const tempConfig = {
                ...((existing?.temp_config && typeof existing.temp_config === "object") ? existing.temp_config : {}),
                mitarbeiterImport: {
                    betrieb: row.betrieb,
                    bereich: row.bereich,
                    stundenMonat: row.stundenMonat,
                    urlaub: row.urlaub,
                    quelle: path.basename(options.workbookPath),
                    importiertAm: new Date().toISOString(),
                },
            }

            const payload = {
                tenant_id: options.tenantId,
                branch_id: row.branchId,
                first_name: nameParts.firstName,
                last_name: nameParts.lastName,
                contract_type: row.anstellung || null,
                position: row.position || null,
                qualification: row.bereich || null,
                weekly_working_hours: row.weeklyHours ?? existing?.weekly_working_hours ?? 0,
                annual_paid_leave_days: row.urlaub != null ? Math.round(row.urlaub) : existing?.annual_paid_leave_days ?? null,
                temp_config: tempConfig,
                active: existing?.active ?? true,
            }

            const action: ImportAction = existing ? "update" : "create"
            const fieldChanges = existing ? collectFieldChanges(existing, payload) : []
            const actionPrefix = action === "create" ? "ERSTELLEN" : "AKTUALISIEREN"
            const branchLabel = `${row.betrieb} -> ${row.branchId}`
            const teamLabel = row.teamName ? `${row.teamName} -> ${row.teamId}` : "-"

            if (action === "create") {
                actionLogs.push(
                    `[${actionPrefix}] ${row.mitarbeiter} | Branch ${branchLabel} | Team ${teamLabel} | Vertrag ${row.anstellung || "-"} | Position ${row.position || "-"}`
                )
            } else {
                actionLogs.push(
                    `[${actionPrefix}] ${row.mitarbeiter} | Branch ${branchLabel} | Team ${teamLabel} | Änderungen: ${fieldChanges.length ? fieldChanges.join("; ") : "keine"}`
                )
            }

            if (!existing) {
                if (!options.dryRun) {
                    const [created] = await db
                        .insert(authProfiles)
                        .values(payload)
                        .returning()

                    if (!created) {
                        throw new Error(`Profil für "${row.mitarbeiter}" konnte nicht erstellt werden.`)
                    }

                    await db.insert(authProfileBranches).values({
                        profile_id: created.id,
                        branch_id: row.branchId,
                    })

                    if (row.teamId) {
                        await db.insert(authProfileTeams).values({
                            profile_id: created.id,
                            team_id: row.teamId,
                        })
                    }

                    existingByName.set(nameKey, created)
                }

                createdProfiles += 1
                continue
            }

            if (!options.dryRun) {
                await db
                    .update(authProfiles)
                    .set(payload)
                    .where(
                        and(
                            eq(authProfiles.id, existing.id),
                            eq(authProfiles.tenant_id, options.tenantId)
                        )
                    )

                await db
                    .delete(authProfileBranches)
                    .where(eq(authProfileBranches.profile_id, existing.id))

                await db.insert(authProfileBranches).values({
                    profile_id: existing.id,
                    branch_id: row.branchId,
                })

                await db
                    .delete(authProfileTeams)
                    .where(eq(authProfileTeams.profile_id, existing.id))

                if (row.teamId) {
                    await db.insert(authProfileTeams).values({
                        profile_id: existing.id,
                        team_id: row.teamId,
                    })
                }
            }

            updatedProfiles += 1
        }

        console.log("")
        console.log(`[IMPORT MITARBEITER] Tenant: ${tenant.id} (${tenant.name})`)
        console.log(`[IMPORT MITARBEITER] Datei: ${options.workbookPath}`)
        console.log(`[IMPORT MITARBEITER] Dry-Run: ${options.dryRun ? "JA" : "NEIN"}`)
        console.log(`[IMPORT MITARBEITER] Zeilen gelesen: ${rows.length}`)
        console.log(`[IMPORT MITARBEITER] Profile erstellt: ${createdProfiles}`)
        console.log(`[IMPORT MITARBEITER] Profile aktualisiert: ${updatedProfiles}`)
        if (actionLogs.length) {
            console.log("[IMPORT MITARBEITER] Details:")
            for (const logLine of actionLogs) {
                console.log(`  ${logLine}`)
            }
        }
        console.log("")
    } finally {
        await pool.end()
    }
}

main().catch((error) => {
    console.error("[IMPORT MITARBEITER] Fehler:", error)
    process.exitCode = 1
})
