import { FastifyInstance } from "fastify"
import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"
import archiver from "archiver"
import { createReadStream, createWriteStream } from "fs"
import { stat, unlink } from "fs/promises"
import os from "os"
import path from "path"
import { BlobReader, TextWriter, Uint8ArrayWriter, ZipReader } from "@zip.js/zip.js"

import { pool } from "../../db"
import { s3 } from "./s3"
import { secrets } from "./secrets"
import { decrypt, encrypt } from "./crypt"

type TableRows = Record<string, Record<string, any>[]>
type TableMetadata = {
    columns: string[]
    jsonColumns: Set<string>
    generatedColumns: Set<string>
}

export type TenantFullExport = {
    format: "fedeo.tenant-full-export"
    version: 1
    exportedAt: string
    tenantId: number
    tables: TableRows
    files: {
        id: string
        path: string
        name: string | null
        mimeType: string | null
        size: number | null
        contentBase64: string | null
        missing?: boolean
        error?: string
    }[]
}

type ImportResult = {
    tenantId: number
    tables: { table: string; rows: number }[]
    files: { restored: number; skipped: number }
}

type ImportOptions = {
    targetTenantId?: number | null
    onProgress?: (progress: { done: number; total: number; message?: string }) => Promise<void> | void
}

type BuildTenantFullExportOptions = {
    includeFileContents?: boolean
}

type TenantArchiveManifest = {
    format: "fedeo.tenant-archive-export"
    version: 1
    exportedAt: string
    tenantId: number
    tables: { name: string; rows: number; path: string }[]
    files: {
        id: string
        path: string
        archivePath: string | null
        name: string | null
        mimeType: string | null
        size: number | null
        missing?: boolean
        error?: string
    }[]
}

type ArchiveExportOptions = {
    filename?: string
    storagePath?: string
    onProgress?: (progress: { filesDone: number; filesTotal: number }) => Promise<void> | void
}

const ENTITY_BANKACCOUNT_PLAIN_FIELDS = {
    iban: "__plainIban",
    bic: "__plainBic",
    bankName: "__plainBankName",
}

const quoteIdent = (value: string) => `"${value.replace(/"/g, '""')}"`
const matrixServerName = () =>
    process.env.MATRIX_SERVER_NAME ||
    secrets.MATRIX_SERVER_NAME ||
    process.env.DOMAIN ||
    "localhost"

const normalizeMatrixLocalpartSeed = (value: string) => {
    const normalized = value
        .toLowerCase()
        .normalize("NFKD")
        .replace(/[\u0300-\u036f]/g, "")
        .replace(/ä/g, "a")
        .replace(/ö/g, "o")
        .replace(/ü/g, "u")
        .replace(/ß/g, "ss")
        .replace(/[^a-z0-9._=-]+/g, "_")
        .replace(/_+/g, "_")
        .replace(/^[._=-]+|[._=-]+$/g, "")

    return normalized || "user"
}

const normalizeMatrixAliasSeed = (value: string) =>
    normalizeMatrixLocalpartSeed(value)
        .replace(/[.=]/g, "_")
        .replace(/_+/g, "_")

const tenantRoomAliasLocalpart = (
    tenant: { id: number, short?: string | null, name?: string | null },
    roomKey: string
) => {
    const tenantSeed = normalizeMatrixAliasSeed(tenant.short || tenant.name || `tenant_${tenant.id}`)
    const roomSeed = normalizeMatrixAliasSeed(roomKey)
    return `fedeo_${tenantSeed}_${tenant.id}_${roomSeed}`
}

const tenantRoomAlias = (
    tenant: { id: number, short?: string | null, name?: string | null },
    roomKey: string
) => `#${tenantRoomAliasLocalpart(tenant, roomKey)}:${matrixServerName()}`

const tableColumns = async (client: any) => {
    const result = await client.query(`
        select table_name, column_name, data_type, is_generated
        from information_schema.columns
        where table_schema = 'public'
        order by table_name, ordinal_position
    `)

    const columnsByTable = new Map<string, TableMetadata>()
    for (const row of result.rows) {
        const metadata = columnsByTable.get(row.table_name) || {
            columns: [],
            jsonColumns: new Set<string>(),
            generatedColumns: new Set<string>(),
        }

        metadata.columns.push(row.column_name)
        if (row.data_type === "json" || row.data_type === "jsonb") {
            metadata.jsonColumns.add(row.column_name)
        }
        if (row.is_generated === "ALWAYS") {
            metadata.generatedColumns.add(row.column_name)
        }

        columnsByTable.set(row.table_name, metadata)
    }

    return columnsByTable
}

const loadRows = async (client: any, table: string, whereSql: string, params: any[] = []) => {
    const result = await client.query(`select * from ${quoteIdent(table)} where ${whereSql}`, params)

    return result.rows
}

const collectIds = (rows: Record<string, any>[], column: string) => {
    return Array.from(new Set(rows.map((row) => row[column]).filter(Boolean)))
}

const addRows = (tables: TableRows, table: string, rows: Record<string, any>[]) => {
    if (!rows.length) {
        if (!tables[table]) tables[table] = []
        return
    }

    const existingRows = tables[table] || []
    const existingKeys = new Set(existingRows.map((row) => JSON.stringify(row)))
    for (const row of rows) {
        const key = JSON.stringify(row)
        if (!existingKeys.has(key)) {
            existingRows.push(row)
            existingKeys.add(key)
        }
    }
    tables[table] = existingRows
}

const decryptEntityBankAccountsForExport = (rows: Record<string, any>[] = []) => {
    return rows.map((row) => {
        const nextRow = { ...row }

        try {
            nextRow[ENTITY_BANKACCOUNT_PLAIN_FIELDS.iban] = row.iban_encrypted ? decrypt(row.iban_encrypted) : null
            nextRow[ENTITY_BANKACCOUNT_PLAIN_FIELDS.bic] = row.bic_encrypted ? decrypt(row.bic_encrypted) : null
            nextRow[ENTITY_BANKACCOUNT_PLAIN_FIELDS.bankName] = row.bank_name_encrypted ? decrypt(row.bank_name_encrypted) : null
        } catch (err: any) {
            throw new Error(`Bankverbindung ${row.id || ""} konnte für den Export nicht entschlüsselt werden: ${err?.message || err}`)
        }

        return nextRow
    })
}

const isMissingObjectError = (err: any) =>
    err?.Code === "NoSuchKey" ||
    err?.name === "NoSuchKey" ||
    err?.$metadata?.httpStatusCode === 404

const loadObjectAsBase64 = async (path: string) => {
    try {
        const { Body } = await s3.send(new GetObjectCommand({
            Bucket: secrets.S3_BUCKET,
            Key: path,
        }))

        const chunks: Buffer[] = []
        for await (const chunk of Body as any) {
            chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
        }

        return {
            contentBase64: Buffer.concat(chunks).toString("base64"),
            missing: false,
            error: null,
        }
    } catch (err: any) {
        if (!isMissingObjectError(err)) throw err

        return {
            contentBase64: null,
            missing: true,
            error: err?.Code || err?.name || "NoSuchKey",
        }
    }
}

const loadObjectStream = async (path: string) => {
    try {
        const { Body } = await s3.send(new GetObjectCommand({
            Bucket: secrets.S3_BUCKET,
            Key: path,
        }))

        return {
            body: Body as any,
            missing: false,
            error: null,
        }
    } catch (err: any) {
        if (!isMissingObjectError(err)) throw err

        return {
            body: null,
            missing: true,
            error: err?.Code || err?.name || "NoSuchKey",
        }
    }
}

const archiveEntryPathForFile = (path: string) => {
    const normalizedPath = path
        .replace(/\\/g, "/")
        .replace(/^\/+/, "")
        .split("/")
        .filter((part) => part && part !== "." && part !== "..")
        .join("/")

    return `files/${normalizedPath || "unnamed-file"}`
}

const filenameFromPath = (path: string) => {
    const filename = path
        .replace(/\\/g, "/")
        .split("/")
        .filter(Boolean)
        .pop()

    if (!filename) return null

    try {
        return decodeURIComponent(filename)
    } catch {
        return filename
    }
}

const jsonBuffer = (value: unknown) => Buffer.from(`${JSON.stringify(value, null, 2)}\n`, "utf8")

export const buildTenantFullExport = async (
    server: FastifyInstance,
    tenantId: number,
    options: BuildTenantFullExportOptions = {}
): Promise<TenantFullExport> => {
    const client = await pool.connect()
    const includeFileContents = options.includeFileContents !== false

    try {
        const columnsByTable = await tableColumns(client)
        const tables: TableRows = {}

        const tenantRows = await loadRows(client, "tenants", "id = $1", [tenantId])
        if (!tenantRows.length) throw new Error("Tenant nicht gefunden")

        addRows(tables, "tenants", tenantRows)

        for (const [table, metadata] of columnsByTable.entries()) {
            if (table === "tenants") continue
            const { columns } = metadata

            const tenantColumn = columns.includes("tenant")
                ? "tenant"
                : columns.includes("tenant_id")
                    ? "tenant_id"
                    : null

            if (!tenantColumn) continue

            const rows = await loadRows(client, table, `${quoteIdent(tenantColumn)} = $1`, [tenantId])
            addRows(tables, table, rows)
        }

        const profileIds = collectIds(tables.auth_profiles || [], "id")
        const userIds = Array.from(new Set([
            ...collectIds(tables.auth_tenant_users || [], "user_id"),
            ...collectIds(tables.auth_profiles || [], "user_id"),
            ...collectIds(tables.auth_user_roles || [], "user_id"),
        ]))
        const roleIds = Array.from(new Set([
            ...collectIds(tables.auth_roles || [], "id"),
            ...collectIds(tables.auth_user_roles || [], "role_id"),
        ]))

        if (userIds.length) {
            addRows(tables, "auth_users", await loadRows(client, "auth_users", "id = any($1::uuid[])", [userIds]))
        }

        if (roleIds.length) {
            addRows(tables, "auth_roles", await loadRows(client, "auth_roles", "id = any($1::uuid[])", [roleIds]))
            addRows(tables, "auth_role_permissions", await loadRows(client, "auth_role_permissions", "role_id = any($1::uuid[])", [roleIds]))
        }

        if (profileIds.length) {
            addRows(tables, "auth_profile_branches", await loadRows(client, "auth_profile_branches", "profile_id = any($1::uuid[])", [profileIds]))
            addRows(tables, "auth_profile_teams", await loadRows(client, "auth_profile_teams", "profile_id = any($1::uuid[])", [profileIds]))
        }

        if (tables.entitybankaccounts?.length) {
            tables.entitybankaccounts = decryptEntityBankAccountsForExport(tables.entitybankaccounts)
        }

        const files = []
        const filePaths = new Set<string>()
        const addExportFile = async (file: {
            id: string
            path: string
            name?: string | null
            mimeType?: string | null
            size?: number | null
        }) => {
            if (!file.path || filePaths.has(file.path)) return
            filePaths.add(file.path)

            const object = includeFileContents
                ? await loadObjectAsBase64(file.path)
                : { contentBase64: null, missing: false, error: null }
            if (object.missing) {
                server.log.warn({
                    fileId: file.id,
                    path: file.path,
                    bucket: secrets.S3_BUCKET,
                    error: object.error,
                }, "Tenant full export skipped missing S3 object")
            }

            files.push({
                id: file.id,
                path: file.path,
                name: file.name || filenameFromPath(file.path),
                mimeType: file.mimeType || null,
                size: file.size || null,
                contentBase64: object.contentBase64,
                missing: object.missing || undefined,
                error: object.error || undefined,
            })
        }

        for (const file of tables.files || []) {
            if (!file.path) continue

            await addExportFile({
                id: file.id,
                path: file.path,
                name: file.name || null,
                mimeType: file.mimeType || null,
                size: file.size || null,
            })
        }

        for (const letterhead of tables.letterheads || []) {
            if (!letterhead.path) continue

            await addExportFile({
                id: `letterhead:${letterhead.id}`,
                path: letterhead.path,
                name: filenameFromPath(letterhead.path),
                mimeType: "application/pdf",
                size: null,
            })
        }

        return {
            format: "fedeo.tenant-full-export",
            version: 1,
            exportedAt: new Date().toISOString(),
            tenantId,
            tables,
            files,
        }
    } finally {
        client.release()
    }
}

export const createTenantFullExportArchive = async (
    server: FastifyInstance,
    tenantId: number,
    options: ArchiveExportOptions = {}
) => {
    const exportData = await buildTenantFullExport(server, tenantId, { includeFileContents: false })
    const safeTenantName = String(exportData.tables.tenants?.[0]?.short || exportData.tables.tenants?.[0]?.name || tenantId)
        .replace(/[^a-z0-9_-]+/gi, "-")
        .replace(/^-+|-+$/g, "")
        .toLowerCase()
    const filename = options.filename || `fedeo-tenant-${safeTenantName || tenantId}-${new Date().toISOString().slice(0, 10)}.fedeo-export.zip`
    const storagePath = options.storagePath || `tenant-exports/${tenantId}/${Date.now()}-${filename}`
    const fileRows = exportData.files || []
    const archive = archiver("zip", { zlib: { level: 6 } })
    const tempPath = path.join(os.tmpdir(), `fedeo-tenant-export-${tenantId}-${Date.now()}-${filename}`)
    const output = createWriteStream(tempPath)
    const archiveComplete = new Promise<void>((resolve, reject) => {
        output.on("close", resolve)
        output.on("error", reject)
        archive.on("error", reject)
        archive.on("warning", (err) => {
            if ((err as NodeJS.ErrnoException).code === "ENOENT") return
            reject(err)
        })
    })

    archive.pipe(output)

    try {
        const tables = Object.entries(exportData.tables)
            .sort(([a], [b]) => a.localeCompare(b))
            .map(([table, rows]) => {
                const path = `tables/${table}.json`
                archive.append(jsonBuffer(rows), { name: path })
                return { name: table, rows: rows.length, path }
            })

        const manifestFiles: TenantArchiveManifest["files"] = []
        let filesDone = 0
        const filesTotal = fileRows.filter((file) => file.path).length

        for (const file of fileRows) {
            if (!file.path) continue

            const archivePath = archiveEntryPathForFile(file.path)
            const object = await loadObjectStream(file.path)

            if (object.missing) {
                server.log.warn({
                    fileId: file.id,
                    path: file.path,
                    bucket: secrets.S3_BUCKET,
                    error: object.error,
                }, "Tenant archive export skipped missing S3 object")
            } else {
                archive.append(object.body, { name: archivePath })
            }

            filesDone += 1
            manifestFiles.push({
                id: file.id,
                path: file.path,
                archivePath: object.missing ? null : archivePath,
                name: file.name || null,
                mimeType: file.mimeType || null,
                size: file.size || null,
                missing: object.missing || undefined,
                error: object.error || undefined,
            })

            if (filesDone % 10 === 0 || filesDone === filesTotal) {
                await options.onProgress?.({ filesDone, filesTotal })
            }
        }

        const manifest: TenantArchiveManifest = {
            format: "fedeo.tenant-archive-export",
            version: 1,
            exportedAt: exportData.exportedAt,
            tenantId,
            tables,
            files: manifestFiles,
        }

        archive.append(jsonBuffer(manifest), { name: "manifest.json" })
        await archive.finalize()
        await archiveComplete

        const { size } = await stat(tempPath)
        await s3.send(new PutObjectCommand({
            Bucket: secrets.S3_BUCKET,
            Key: storagePath,
            Body: createReadStream(tempPath),
            ContentLength: size,
            ContentType: "application/zip",
            ContentDisposition: `attachment; filename="${filename}"`,
        }))

        return {
            filename,
            storagePath,
            contentType: "application/zip",
            size,
            filesTotal,
            filesDone,
        }
    } finally {
        await unlink(tempPath).catch(() => undefined)
    }
}

const restoreFiles = async (exportData: TenantFullExport) => {
    let restored = 0
    let skipped = 0

    for (const file of exportData.files || []) {
        if (!file.path || !file.contentBase64) {
            skipped += 1
            continue
        }

        const body = Buffer.from(file.contentBase64, "base64")
        await s3.send(new PutObjectCommand({
            Bucket: secrets.S3_BUCKET,
            Key: file.path,
            Body: body,
            ContentType: file.mimeType || "application/octet-stream",
            ContentLength: body.length,
        }))
        restored += 1
    }

    return { restored, skipped }
}

const readZipTextEntry = async (entriesByName: Map<string, any>, name: string) => {
    const entry = entriesByName.get(name)
    if (!entry) throw new Error(`${name} fehlt im Mandantenexport`)

    return entry.getData(new TextWriter())
}

const restoreArchiveFiles = async (
    entriesByName: Map<string, any>,
    exportData: TenantFullExport,
    manifest: TenantArchiveManifest
) => {
    let restored = 0
    let skipped = 0
    const filesByPath = new Map((manifest.files || []).map((file) => [file.path, file]))

    for (const fileRow of exportData.files || []) {
        const originalPath = fileRow.path
        if (!originalPath) {
            skipped += 1
            continue
        }

        const manifestFile = filesByPath.get(originalPath)
        const archivePath = manifestFile?.archivePath
        if (!archivePath || manifestFile?.missing) {
            skipped += 1
            continue
        }

        const entry = entriesByName.get(archivePath)
        if (!entry) {
            skipped += 1
            continue
        }

        const content = await entry.getData(new Uint8ArrayWriter())
        await s3.send(new PutObjectCommand({
            Bucket: secrets.S3_BUCKET,
            Key: fileRow.path,
            Body: Buffer.from(content),
            ContentType: fileRow.mimeType || manifestFile.mimeType || "application/octet-stream",
            ContentLength: content.length,
        }))
        restored += 1
    }

    return { restored, skipped }
}

const remapTenantScopedExport = (
    exportData: TenantFullExport,
    targetTenantId?: number | null
): TenantFullExport => {
    if (!targetTenantId || targetTenantId === exportData.tenantId) return exportData

    const sourceTenantId = exportData.tenantId
    const sourcePathPrefix = `${sourceTenantId}/`
    const targetPathPrefix = `${targetTenantId}/`
    const tables: TableRows = {}

    for (const [table, rows] of Object.entries(exportData.tables || {})) {
        tables[table] = rows.map((row) => {
            const nextRow = { ...row }

            if (table === "tenants" && nextRow.id === sourceTenantId) {
                nextRow.id = targetTenantId
            }

            if (nextRow.tenant === sourceTenantId) {
                nextRow.tenant = targetTenantId
            }

            if (nextRow.tenant_id === sourceTenantId) {
                nextRow.tenant_id = targetTenantId
            }

            if (table === "files" && typeof nextRow.path === "string" && nextRow.path.startsWith(sourcePathPrefix)) {
                nextRow.path = `${targetPathPrefix}${nextRow.path.slice(sourcePathPrefix.length)}`
            }

            if (table === "letterheads" && typeof nextRow.path === "string" && nextRow.path.startsWith(sourcePathPrefix)) {
                nextRow.path = `${targetPathPrefix}${nextRow.path.slice(sourcePathPrefix.length)}`
            }

            return nextRow
        })
    }

    return {
        ...exportData,
        tenantId: targetTenantId,
        tables,
        files: (exportData.files || []).map((file) => ({
            ...file,
            path: file.path?.startsWith(sourcePathPrefix)
                ? `${targetPathPrefix}${file.path.slice(sourcePathPrefix.length)}`
                : file.path,
        })),
    }
}

const encryptEntityBankAccountRowsForImport = (exportData: TenantFullExport) => {
    const rows = exportData.tables.entitybankaccounts || []

    for (const row of rows) {
        const plainIban = row[ENTITY_BANKACCOUNT_PLAIN_FIELDS.iban]
        const plainBic = row[ENTITY_BANKACCOUNT_PLAIN_FIELDS.bic]
        const plainBankName = row[ENTITY_BANKACCOUNT_PLAIN_FIELDS.bankName]

        if (typeof plainIban === "string" && typeof plainBic === "string" && typeof plainBankName === "string") {
            row.iban_encrypted = encrypt(plainIban)
            row.bic_encrypted = encrypt(plainBic)
            row.bank_name_encrypted = encrypt(plainBankName)
        }

        delete row[ENTITY_BANKACCOUNT_PLAIN_FIELDS.iban]
        delete row[ENTITY_BANKACCOUNT_PLAIN_FIELDS.bic]
        delete row[ENTITY_BANKACCOUNT_PLAIN_FIELDS.bankName]
    }
}

const prepareCommunicationRoomsForImport = (exportData: TenantFullExport) => {
    const rows = exportData.tables.communication_rooms || []
    if (!rows.length) return

    const tenantById = new Map((exportData.tables.tenants || []).map((tenant) => [
        Number(tenant.id),
        {
            id: Number(tenant.id),
            name: tenant.name,
            short: tenant.short,
        },
    ]))

    for (const row of rows) {
        const tenantId = Number(row.tenant_id)
        const tenant = tenantById.get(tenantId)

        row.matrix_room_id = null
        row.parent_space_room_id = null

        if (tenant && row.key) {
            row.matrix_alias = tenantRoomAlias(tenant, String(row.key))
        } else {
            row.matrix_alias = null
        }
    }
}

const cleanupImportedCommunicationRooms = async (client: any, exportData: TenantFullExport) => {
    const rows = exportData.tables.communication_rooms || []
    if (!rows.length) return 0

    const tenantById = new Map((exportData.tables.tenants || []).map((tenant) => [
        Number(tenant.id),
        {
            id: Number(tenant.id),
            name: tenant.name,
            short: tenant.short,
        },
    ]))
    let cleaned = 0

    for (const row of rows) {
        const tenantId = Number(row.tenant_id)
        const key = String(row.key || "")
        const tenant = tenantById.get(tenantId)
        if (!tenantId || !key || !tenant) continue

        const alias = tenantRoomAlias(tenant, key)
        const result = await client.query(
            `
                update communication_rooms
                set matrix_room_id = null,
                    parent_space_room_id = null,
                    matrix_alias = $3,
                    updated_at = now()
                where tenant_id = $1 and key = $2
            `,
            [tenantId, key, alias]
        )

        cleaned += result.rowCount || 0
    }

    return cleaned
}

const prepareColumnValue = (value: any, isJsonColumn: boolean) => {
    if (!isJsonColumn || value === null || typeof value === "undefined") return value
    if (typeof value === "string") return value

    return JSON.stringify(value)
}

const insertRows = async (client: any, table: string, rows: Record<string, any>[], metadata: TableMetadata) => {
    if (!rows.length) return 0

    let inserted = 0
    const availableColumns = new Set(metadata.columns.filter((column) => !metadata.generatedColumns.has(column)))

    for (const row of rows) {
        const rowColumns = Object.keys(row).filter((column) => availableColumns.has(column))
        if (!rowColumns.length) continue

        const placeholders = rowColumns.map((_, index) => `$${index + 1}`).join(", ")
        const values = rowColumns.map((column) => prepareColumnValue(row[column], metadata.jsonColumns.has(column)))

        await client.query(
            `insert into ${quoteIdent(table)} (${rowColumns.map(quoteIdent).join(", ")}) values (${placeholders}) on conflict do nothing`,
            values
        )
        inserted += 1
    }

    return inserted
}

const insertAuthTenantUserRows = async (client: any, rows: Record<string, any>[], metadata: TableMetadata) => {
    if (!rows.length) return 0

    let inserted = 0
    const availableColumns = new Set(metadata.columns.filter((column) => !metadata.generatedColumns.has(column)))

    for (const row of rows) {
        if (!row.tenant_id || !row.user_id) continue

        const existing = await client.query(
            `select 1 from ${quoteIdent("auth_tenant_users")} where ${quoteIdent("tenant_id")} = $1 and ${quoteIdent("user_id")} = $2 limit 1`,
            [row.tenant_id, row.user_id]
        )
        if (existing.rows.length) continue

        const rowColumns = Object.keys(row).filter((column) => availableColumns.has(column))
        if (!rowColumns.length) continue

        const placeholders = rowColumns.map((_, index) => `$${index + 1}`).join(", ")
        const values = rowColumns.map((column) => prepareColumnValue(row[column], metadata.jsonColumns.has(column)))

        await client.query(
            `insert into ${quoteIdent("auth_tenant_users")} (${rowColumns.map(quoteIdent).join(", ")}) values (${placeholders})`,
            values
        )
        inserted += 1
    }

    return inserted
}

const insertIdentityRowsWithRemap = async (
    client: any,
    table: string,
    rows: Record<string, any>[],
    metadata: TableMetadata
) => {
    if (!rows.length) return { count: 0, idMap: new Map<number, number>() }

    let count = 0
    const idMap = new Map<number, number>()
    const availableColumns = new Set(metadata.columns.filter((column) => !metadata.generatedColumns.has(column)))

    for (const row of rows) {
        const sourceId = Number(row.id)
        if (!sourceId) continue

        const existing = await client.query(`select * from ${quoteIdent(table)} where ${quoteIdent("id")} = $1 limit 1`, [sourceId])
        const existingTenant = existing.rows[0]?.tenant ?? existing.rows[0]?.tenant_id
        const rowTenant = row.tenant ?? row.tenant_id
        const shouldPreserveId = existing.rows.length === 0
        const shouldReuseExisting = existing.rows.length > 0 && rowTenant && existingTenant === rowTenant
        const rowForInsert = shouldPreserveId ? row : { ...row, id: undefined }
        const rowColumns = Object
            .keys(rowForInsert)
            .filter((column) => availableColumns.has(column) && typeof rowForInsert[column] !== "undefined")

        if (shouldReuseExisting) {
            idMap.set(sourceId, sourceId)
            continue
        }

        if (!rowColumns.length) continue

        const placeholders = rowColumns.map((_, index) => `$${index + 1}`).join(", ")
        const values = rowColumns.map((column) => prepareColumnValue(rowForInsert[column], metadata.jsonColumns.has(column)))
        const inserted = await client.query(
            `insert into ${quoteIdent(table)} (${rowColumns.map(quoteIdent).join(", ")}) values (${placeholders}) returning ${quoteIdent("id")}`,
            values
        )
        const targetId = Number(inserted.rows[0]?.id)
        if (targetId) {
            idMap.set(sourceId, targetId)
        }
        count += 1
    }

    return { count, idMap }
}

const remapTableColumn = (rows: Record<string, any>[] = [], column: string, idMap: Map<number, number>) => {
    if (!idMap.size) return

    for (const row of rows) {
        const currentValue = Number(row[column])
        const mappedValue = idMap.get(currentValue)
        if (mappedValue) row[column] = mappedValue
    }
}

const refreshSequences = async (client: any, columnsByTable: Map<string, TableMetadata>) => {
    for (const [table, metadata] of columnsByTable.entries()) {
        const { columns } = metadata
        if (!columns.includes("id")) continue

        const sequenceResult = await client.query("select pg_get_serial_sequence($1, $2) as sequence_name", [`public.${table}`, "id"])
        const sequenceName = sequenceResult.rows[0]?.sequence_name
        if (!sequenceName) continue

        await client.query(`
            select setval(
                $1::regclass,
                greatest(coalesce((select max(id) from ${quoteIdent(table)}), 1), 1),
                true
            )
        `, [sequenceName])
    }
}

export const importTenantFullExport = async (
    server: FastifyInstance,
    rawExportData: TenantFullExport,
    options: ImportOptions = {}
): Promise<ImportResult> => {
    if (rawExportData?.format !== "fedeo.tenant-full-export" || rawExportData.version !== 1) {
        throw new Error("Ungültiges FEDEO Mandantenexport-Format")
    }

    const exportData = remapTenantScopedExport(rawExportData, options.targetTenantId)
    encryptEntityBankAccountRowsForImport(exportData)
    prepareCommunicationRoomsForImport(exportData)
    const client = await pool.connect()
    const importOrder = [
        "tenants",
        "auth_users",
        "auth_roles",
        "auth_role_permissions",
        "auth_tenant_users",
        "auth_profiles",
        "auth_user_roles",
        "auth_profile_branches",
        "auth_profile_teams",
    ]

    try {
        const columnsByTable = await tableColumns(client)
        const tableNames = [
            ...importOrder,
            ...Object.keys(exportData.tables).filter((table) => !importOrder.includes(table)).sort(),
        ].filter((table, index, all) => all.indexOf(table) === index)
        const specialTables = [
            columnsByTable.has("auth_tenant_users") ? "auth_tenant_users" : null,
            columnsByTable.has("bankaccounts") ? "bankaccounts" : null,
            columnsByTable.has("bankstatements") ? "bankstatements" : null,
            columnsByTable.has("letterheads") ? "letterheads" : null,
        ].filter(Boolean) as string[]
        const regularTableNames = tableNames.filter((table) => !specialTables.includes(table))
        const progressTotal = Math.max(1, specialTables.length + regularTableNames.length + 3)
        let progressDone = 0
        const reportProgress = async (message: string) => {
            await options.onProgress?.({
                done: Math.min(progressDone, progressTotal),
                total: progressTotal,
                message,
            })
        }

        await client.query("begin")
        await client.query("set local session_replication_role = replica")
        await reportProgress("Dateien werden vorbereitet")
        const files = await restoreFiles(exportData)
        progressDone += 1
        await reportProgress("Dateien vorbereitet")

        const importedTables: { table: string; rows: number }[] = []
        const authTenantUsersMetadata = columnsByTable.get("auth_tenant_users")
        if (authTenantUsersMetadata) {
            const count = await insertAuthTenantUserRows(client, exportData.tables.auth_tenant_users || [], authTenantUsersMetadata)
            importedTables.push({ table: "auth_tenant_users", rows: count })
            progressDone += 1
            await reportProgress("Tenant-Zuordnungen importiert")
        }

        const bankaccountMetadata = columnsByTable.get("bankaccounts")
        if (bankaccountMetadata) {
            const result = await insertIdentityRowsWithRemap(client, "bankaccounts", exportData.tables.bankaccounts || [], bankaccountMetadata)
            remapTableColumn(exportData.tables.bankstatements, "account", result.idMap)
            importedTables.push({ table: "bankaccounts", rows: result.count })
            progressDone += 1
            await reportProgress("Bankkonten importiert")
        }

        const bankstatementMetadata = columnsByTable.get("bankstatements")
        if (bankstatementMetadata) {
            const result = await insertIdentityRowsWithRemap(client, "bankstatements", exportData.tables.bankstatements || [], bankstatementMetadata)
            remapTableColumn(exportData.tables.statementallocations, "bs_id", result.idMap)
            remapTableColumn(exportData.tables.historyitems, "bankstatement", result.idMap)
            importedTables.push({ table: "bankstatements", rows: result.count })
            progressDone += 1
            await reportProgress("Bankauszüge importiert")
        }

        const letterheadMetadata = columnsByTable.get("letterheads")
        if (letterheadMetadata) {
            const result = await insertIdentityRowsWithRemap(client, "letterheads", exportData.tables.letterheads || [], letterheadMetadata)
            remapTableColumn(exportData.tables.createddocuments, "letterhead", result.idMap)
            importedTables.push({ table: "letterheads", rows: result.count })
            progressDone += 1
            await reportProgress("Briefpapiere importiert")
        }

        for (const table of regularTableNames) {
            const rows = exportData.tables[table] || []
            const metadata = columnsByTable.get(table)
            if (!metadata) continue

            const count = await insertRows(client, table, rows, metadata)
            importedTables.push({ table, rows: count })
            progressDone += 1
            await reportProgress(`${table} importiert`)
        }

        const cleanedCommunicationRooms = await cleanupImportedCommunicationRooms(client, exportData)
        if (cleanedCommunicationRooms) {
            importedTables.push({ table: "communication_rooms_matrix_reset", rows: cleanedCommunicationRooms })
        }
        progressDone += 1
        await reportProgress("Kommunikationsräume bereinigt")

        await refreshSequences(client, columnsByTable)
        progressDone = progressTotal
        await reportProgress("Import abgeschlossen")
        await client.query("commit")

        return {
            tenantId: exportData.tenantId,
            tables: importedTables,
            files,
        }
    } catch (err) {
        await client.query("rollback")
        throw err
    } finally {
        client.release()
    }
}

export const importTenantFullExportArchive = async (
    server: FastifyInstance,
    archiveBuffer: Buffer,
    options: ImportOptions = {}
): Promise<ImportResult> => {
    const reader = new ZipReader(new BlobReader(new Blob([archiveBuffer])))

    try {
        const entries = await reader.getEntries()
        const entriesByName = new Map(entries.map((entry: any) => [entry.filename, entry]))
        const manifest = JSON.parse(await readZipTextEntry(entriesByName, "manifest.json")) as TenantArchiveManifest

        if (manifest?.format !== "fedeo.tenant-archive-export" || manifest.version !== 1) {
            throw new Error("Ungültiges FEDEO Mandantenarchiv-Format")
        }

        const tables: TableRows = {}
        for (const table of manifest.tables || []) {
            tables[table.name] = JSON.parse(await readZipTextEntry(entriesByName, table.path))
        }

        const rawExportData: TenantFullExport = {
            format: "fedeo.tenant-full-export",
            version: 1,
            exportedAt: manifest.exportedAt,
            tenantId: manifest.tenantId,
            tables,
            files: (manifest.files || []).map((file) => ({
                id: file.id,
                path: file.path,
                name: file.name,
                mimeType: file.mimeType,
                size: file.size,
                contentBase64: null,
                missing: file.missing,
                error: file.error,
            })),
        }

        const exportData = remapTenantScopedExport(rawExportData, options.targetTenantId)
        const sourceTenantId = rawExportData.tenantId
        const targetTenantId = exportData.tenantId
        const sourcePrefix = `${sourceTenantId}/`
        const targetPrefix = `${targetTenantId}/`
        const remappedManifest: TenantArchiveManifest = {
            ...manifest,
            tenantId: targetTenantId,
            files: (manifest.files || []).map((file) => ({
                ...file,
                path: file.path?.startsWith(sourcePrefix)
                    ? `${targetPrefix}${file.path.slice(sourcePrefix.length)}`
                    : file.path,
            })),
        }

        const result = await importTenantFullExport(server, exportData, { targetTenantId: null })
        const files = await restoreArchiveFiles(entriesByName, exportData, remappedManifest)

        return {
            ...result,
            files,
        }
    } finally {
        await reader.close()
    }
}
