import { FastifyInstance } from "fastify"
import { promises as fs } from "node:fs"
import net from "node:net"
import path from "node:path"
import { randomBytes } from "node:crypto"
import { and, desc, eq, ilike, inArray, ne, or } from "drizzle-orm"
import {
    authProfileBranches,
    authProfiles,
    authProfileTeams,
    authUsers,
    branches,
    teams,
    telephonyCalls,
    telephonyExtensions,
    telephonyTrunks,
} from "../../db/schema"

const envFlag = (value: string | undefined, fallback: boolean) => {
    if (value === undefined || value === "") return fallback
    return ["1", "true", "yes", "on"].includes(value.toLowerCase())
}

const telephonyEnabled = () =>
    envFlag(process.env.TELEPHONY_ENABLED, process.env.NODE_ENV !== "production")

const asteriskHttpStatusUrl = () =>
    process.env.TELEPHONY_ASTERISK_HTTP_URL || ""

const asteriskHttpStatusUrls = () => {
    const configuredUrl = asteriskHttpStatusUrl()
    return configuredUrl ? [configuredUrl] : []
}

const publicAsteriskWsUrl = () =>
    process.env.TELEPHONY_ASTERISK_WS_URL || ""

const defaultAsteriskGeneratedDir = () => {
    const cwd = process.cwd()
    return path.resolve(cwd, cwd.endsWith(`${path.sep}backend`) ? "../telephony/generated" : "telephony/generated")
}

const asteriskGeneratedDir = () =>
    process.env.TELEPHONY_ASTERISK_GENERATED_DIR || defaultAsteriskGeneratedDir()

const defaultAsteriskAmiHost = () =>
    process.env.NODE_ENV === "production" ? "" : "127.0.0.1"

const asteriskAmiConfig = () => ({
    host: process.env.TELEPHONY_ASTERISK_AMI_HOST || defaultAsteriskAmiHost(),
    port: Number(process.env.TELEPHONY_ASTERISK_AMI_PORT || 5038),
    username: process.env.TELEPHONY_ASTERISK_AMI_USER || "fedeo",
    password: process.env.TELEPHONY_ASTERISK_AMI_PASSWORD || "fedeo-ami-dev",
})

const sipDomain = () =>
    process.env.TELEPHONY_SIP_DOMAIN || "localhost"

const trunkProviders = {
    telekom: {
        key: "telekom",
        label: "Telekom",
        defaultRegistrar: "tel.t-online.de",
        aorContactIncludesUser: false,
        contactUserUsesCallerId: false,
    },
    easybell: {
        key: "easybell",
        label: "Easybell",
        defaultRegistrar: "voip.easybell.de",
        aorContactIncludesUser: true,
        contactUserUsesCallerId: true,
    },
} as const

type TrunkProviderKey = keyof typeof trunkProviders

const normalizeTrunkProvider = (provider: string | null | undefined): TrunkProviderKey =>
    provider === "easybell" ? "easybell" : "telekom"

const trunkProviderConfig = (provider: string | null | undefined) =>
    trunkProviders[normalizeTrunkProvider(provider)]

const sanitizeTrunk = (trunk: any) => ({
    id: trunk?.id || null,
    provider: normalizeTrunkProvider(trunk?.provider),
    enabled: Boolean(trunk?.enabled),
    registrar: trunk?.registrar || trunkProviderConfig(trunk?.provider).defaultRegistrar,
    sipUser: trunk?.sipUser || "",
    authUser: trunk?.authUser || "",
    passwordConfigured: Boolean(trunk?.password),
    callerId: trunk?.callerId || "",
    inboundExtension: trunk?.inboundExtension || "1001",
    defaultRouteExtensionId: trunk?.defaultRouteExtensionId || null,
    outboundPrefix: trunk?.outboundPrefix || "0",
    externalSignalingAddress: trunk?.externalSignalingAddress || "",
    externalMediaAddress: trunk?.externalMediaAddress || "",
    localNetworks: trunk?.localNetworks || "172.16.0.0/12,192.168.0.0/16,10.0.0.0/8",
})

const envExternalTelephonyConfig = () => {
    const provider = process.env.TELEPHONY_EXTERNAL_PROVIDER || (
        envFlag(process.env.TELEPHONY_TELEKOM_ENABLED, false) ? "telekom" : ""
    )
    const enabled = envFlag(
        process.env.TELEPHONY_EXTERNAL_ENABLED,
        envFlag(process.env.TELEPHONY_TELEKOM_ENABLED, false)
    )

    return {
        enabled,
        provider: provider || null,
        inboundExtension: process.env.TELEPHONY_EXTERNAL_INBOUND_EXTENSION
            || process.env.TELEPHONY_TELEKOM_INBOUND_EXTENSION
            || "1001",
        outboundPrefix: process.env.TELEPHONY_TELEKOM_OUTBOUND_PREFIX || "0",
        registrar: provider === "telekom" || envFlag(process.env.TELEPHONY_TELEKOM_ENABLED, false)
            ? (process.env.TELEPHONY_TELEKOM_REGISTRAR || "tel.t-online.de")
            : null,
        sipUserConfigured: Boolean(process.env.TELEPHONY_TELEKOM_SIP_USER),
        authUserConfigured: Boolean(process.env.TELEPHONY_TELEKOM_AUTH_USER),
        passwordConfigured: Boolean(process.env.TELEPHONY_TELEKOM_PASSWORD),
        callerIdConfigured: Boolean(process.env.TELEPHONY_TELEKOM_CALLER_ID),
    }
}

const fetchWithTimeout = async (url: string, timeoutMs = 2500) => {
    const controller = new AbortController()
    const timeout = setTimeout(() => controller.abort(), timeoutMs)

    try {
        return await fetch(url, { signal: controller.signal })
    } finally {
        clearTimeout(timeout)
    }
}

const requireTenant = (tenantId: number | null) => {
    if (!tenantId) {
        throw Object.assign(new Error("Kein aktiver Mandant"), { statusCode: 400 })
    }

    return tenantId
}

const bodyString = (body: any, key: string) => {
    const value = body?.[key]
    return typeof value === "string" && value.trim() ? value.trim() : null
}

const bodyNumber = (body: any, key: string) => {
    const value = body?.[key]
    if (value === null || value === undefined || value === "") return null

    const number = Number(value)
    return Number.isFinite(number) ? number : null
}

const normalizeExtensionTargetType = (value: string | null | undefined) => {
    if (value === "team") return "team"
    if (value === "branch") return "branch"
    return "user"
}

const normalizeExtension = (value: string | null | undefined) =>
    asteriskValue(value).replace(/[^0-9*#+]/g, "")

const generateSipPassword = () =>
    randomBytes(18).toString("base64url")

const bodyDate = (body: any, key: string) => {
    const value = body?.[key]
    if (!value) return null

    const date = new Date(value)
    return Number.isNaN(date.getTime()) ? null : date
}

const durationSeconds = (startedAt?: Date | null, endedAt?: Date | null) => {
    if (!startedAt || !endedAt) return null
    return Math.max(0, Math.round((endedAt.getTime() - startedAt.getTime()) / 1000))
}

const asteriskValue = (value: string | null | undefined) =>
    String(value || "").replace(/[\r\n]/g, "").trim()

const renderProviderPjsipConfig = (trunk: any) => {
    const provider = trunkProviderConfig(trunk?.provider)

    if (!trunk?.enabled) {
        return [
            "; Von FEDEO generiert.",
            `; ${provider.label}-Trunk ist deaktiviert.`,
            "",
        ].join("\n")
    }

    const providerKey = provider.key
    const registrar = asteriskValue(trunk.registrar) || provider.defaultRegistrar
    const sipUser = asteriskValue(trunk.sipUser)
    const authUser = asteriskValue(trunk.authUser) || sipUser
    const password = asteriskValue(trunk.password)
    const callerId = asteriskValue(trunk.callerId) || sipUser
    const externalMediaAddress = asteriskValue(trunk.externalMediaAddress || trunk.externalSignalingAddress)
    const contactUser = provider.contactUserUsesCallerId && callerId ? callerId : sipUser
    const aorContact = provider.aorContactIncludesUser && contactUser
        ? `sip:${contactUser}@${registrar}`
        : `sip:${registrar}`

    return [
        "; Von FEDEO generiert. Änderungen im Container können überschrieben werden.",
        `[${providerKey}-auth]`,
        "type=auth",
        "auth_type=userpass",
        `username=${authUser}`,
        `password=${password}`,
        "",
        `[${providerKey}-aor]`,
        "type=aor",
        `contact=${aorContact}`,
        "",
        `[${providerKey}]`,
        "type=endpoint",
        "transport=transport-udp",
        `context=from-${providerKey}`,
        "disallow=all",
        providerKey === "easybell" ? "allow=alaw,ulaw,g722" : "allow=alaw,ulaw",
        `aors=${providerKey}-aor`,
        `outbound_auth=${providerKey}-auth`,
        `from_user=${sipUser}`,
        `from_domain=${registrar}`,
        `callerid=${provider.label} <${callerId}>`,
        ...(externalMediaAddress ? [`media_address=${externalMediaAddress}`] : []),
        "direct_media=no",
        "force_rport=yes",
        "rewrite_contact=yes",
        "rtp_symmetric=yes",
        ...(providerKey === "easybell" ? [
            "ice_support=no",
            "send_pai=yes",
        ] : []),
        "timers=no",
        "",
        `[${providerKey}-identify]`,
        "type=identify",
        `endpoint=${providerKey}`,
        `match=${registrar}`,
        "",
        `[${providerKey}-registration]`,
        "type=registration",
        "transport=transport-udp",
        `outbound_auth=${providerKey}-auth`,
        `server_uri=sip:${registrar}`,
        `client_uri=sip:${sipUser}@${registrar}`,
        `contact_user=${contactUser}`,
        "retry_interval=60",
        `forbidden_retry_interval=${providerKey === "easybell" ? "600" : "300"}`,
        `expiration=${providerKey === "easybell" ? "1800" : "480"}`,
        "line=yes",
        `endpoint=${providerKey}`,
        "",
    ].join("\n")
}

const renderProviderExtensionsConfig = (trunk: any) => {
    const provider = trunkProviderConfig(trunk?.provider)

    if (!trunk?.enabled) {
        return [
            "; Von FEDEO generiert.",
            `; ${provider.label}-Routing ist deaktiviert.`,
            "",
        ].join("\n")
    }

    const providerKey = provider.key
    const inboundExtension = asteriskValue(trunk.inboundExtension) || "1001"
    const outboundPrefix = asteriskValue(trunk.outboundPrefix) || "0"
    const escapedPrefix = outboundPrefix.replace(/[^0-9*#+]/g, "")
    const callerId = asteriskValue(trunk.callerId) || asteriskValue(trunk.sipUser)
    const inboundDid = callerId.replace(/[^0-9+]/g, "")

    return [
        "; Von FEDEO generiert. Änderungen im Container können überschrieben werden.",
        "[fedeo-local]",
        escapedPrefix
            ? `exten => _${escapedPrefix}X.,1,NoOp(FEDEO ausgehend über ${provider.label}: $` + "{EXTEN})"
            : `exten => _X.,1,NoOp(FEDEO ausgehend über ${provider.label}: $` + "{EXTEN})",
        ` same => n,Set(CALLERID(num)=${callerId})`,
        ` same => n,Dial(PJSIP/$` + `{EXTEN}@${providerKey},60)`,
        " same => n,Hangup()",
        "",
        `exten => _+X.,1,NoOp(FEDEO ausgehend über ${provider.label}: $` + "{EXTEN})",
        ` same => n,Set(CALLERID(num)=${callerId})`,
        ` same => n,Dial(PJSIP/$` + `{EXTEN}@${providerKey},60)`,
        " same => n,Hangup()",
        "",
        `[from-${providerKey}]`,
        ...(inboundDid ? [
            `exten => ${inboundDid},1,NoOp(FEDEO eingehend über ${provider.label} DID: $` + "{EXTEN})",
            ` same => n,Dial(PJSIP/${inboundExtension},30)`,
            " same => n,Hangup()",
            "",
        ] : []),
        `exten => s,1,NoOp(FEDEO eingehend über ${provider.label})`,
        ` same => n,Dial(PJSIP/${inboundExtension},30)`,
        " same => n,Hangup()",
        "",
        `exten => _X!,1,NoOp(FEDEO eingehend über ${provider.label}: $` + "{EXTEN})",
        ` same => n,Dial(PJSIP/${inboundExtension},30)`,
        " same => n,Hangup()",
        "",
    ].join("\n")
}

const renderWebRtcConfig = (trunk: any) => {
    const externalMediaAddress = asteriskValue(trunk?.externalMediaAddress || trunk?.externalSignalingAddress)

    if (!externalMediaAddress) {
        return [
            "; Von FEDEO generiert.",
            "; Kein externes WebRTC-Media-Rewrite konfiguriert.",
            "",
        ].join("\n")
    }

    return [
        "; Von FEDEO generiert. Diese Datei wird innerhalb des FEDEO-WebRTC-Templates inkludiert.",
        `media_address=${externalMediaAddress}`,
        "bind_rtp_to_media_address=yes",
        "",
    ].join("\n")
}

const extensionDialTarget = (extension: any, dialTargets: string[]) => {
    const dial = dialTargets.length
        ? `Dial(${dialTargets.map((target) => `PJSIP/${target}`).join("&")},30)`
        : "Hangup()"

    return [
        `exten => ${extension.extension},1,NoOp(FEDEO Nebenstelle ${extension.extension}: ${extension.displayName || extension.targetType})`,
        ` same => n,${dial}`,
        " same => n,Hangup()",
        "",
    ].join("\n")
}

const renderTelephonyExtensionsPjsipConfig = (extensions: any[]) => {
    const userExtensions = extensions.filter((extension) =>
        extension.enabled && extension.targetType === "user"
    )

    if (!userExtensions.length) {
        return [
            "; Von FEDEO generiert.",
            "; Keine Benutzer-Nebenstellen konfiguriert.",
            "",
        ].join("\n")
    }

    return [
        "; Von FEDEO generiert. Änderungen im Container können überschrieben werden.",
        ...userExtensions.flatMap((extension) => {
            const sipUsername = asteriskValue(extension.sipUsername) || extension.extension
            const sipPassword = asteriskValue(extension.sipPassword)
            const displayName = asteriskValue(extension.displayName) || `FEDEO ${extension.extension}`

            return [
                `[${extension.extension}](fedeo-webrtc)`,
                `auth=${extension.extension}-auth`,
                `aors=${extension.extension}`,
                `callerid=${displayName} <${extension.extension}>`,
                "",
                `[${extension.extension}-auth]`,
                "type=auth",
                "auth_type=userpass",
                `username=${sipUsername}`,
                `password=${sipPassword}`,
                "",
                `[${extension.extension}]`,
                "type=aor",
                "max_contacts=5",
                "remove_existing=yes",
                "support_path=yes",
                "",
            ]
        }),
    ].join("\n")
}

const renderTelephonyExtensionsDialplanConfig = (extensions: any[], dialTargetsById: Map<string, string[]>) => {
    const enabledExtensions = extensions.filter((extension) => extension.enabled)

    if (!enabledExtensions.length) {
        return [
            "; Von FEDEO generiert.",
            "; Keine Nebenstellen konfiguriert.",
            "",
        ].join("\n")
    }

    return [
        "; Von FEDEO generiert. Änderungen im Container können überschrieben werden.",
        "[fedeo-local]",
        ...enabledExtensions.map((extension) =>
            extensionDialTarget(extension, dialTargetsById.get(extension.id) || [])
        ),
    ].join("\n")
}

const renderTelekomTransportConfig = (trunk: any) => {
    const externalSignalingAddress = asteriskValue(trunk?.externalSignalingAddress)
    const externalMediaAddress = asteriskValue(trunk?.externalMediaAddress || trunk?.externalSignalingAddress)
    const localNetworks = asteriskValue(trunk?.localNetworks || "172.16.0.0/12,192.168.0.0/16,10.0.0.0/8")
        .split(/[\s,;]+/)
        .map((entry) => entry.trim())
        .filter(Boolean)

    if (!externalSignalingAddress && !externalMediaAddress) {
        return [
            "; Von FEDEO generiert.",
            "; Kein externes Asterisk-NAT-Rewrite konfiguriert.",
            "",
        ].join("\n")
    }

    return [
        "; Von FEDEO generiert. Diese Datei wird innerhalb von [transport-udp] inkludiert.",
        "; Änderungen am Transport benötigen einen Asterisk-Neustart.",
        ...(externalSignalingAddress ? [
            `external_signaling_address=${externalSignalingAddress}`,
            "external_signaling_port=5060",
        ] : []),
        ...(externalMediaAddress ? [`external_media_address=${externalMediaAddress}`] : []),
        ...localNetworks.map((network) => `local_net=${network}`),
        "",
    ].join("\n")
}

const writeAsteriskTrunkConfig = async (trunk: any, extensions: any[] = [], dialTargetsById = new Map<string, string[]>()) => {
    const targetDir = asteriskGeneratedDir()
    await fs.mkdir(targetDir, { recursive: true })

    const files = [
        {
            name: "pjsip.telekom.conf",
            content: renderProviderPjsipConfig(trunk),
        },
        {
            name: "extensions.telekom.conf",
            content: renderProviderExtensionsConfig(trunk),
        },
        {
            name: "pjsip.transport.conf",
            content: renderTelekomTransportConfig(trunk),
        },
        {
            name: "pjsip.webrtc.conf",
            content: renderWebRtcConfig(trunk),
        },
        {
            name: "pjsip.extensions.conf",
            content: renderTelephonyExtensionsPjsipConfig(extensions),
        },
        {
            name: "extensions.fedeo.conf",
            content: renderTelephonyExtensionsDialplanConfig(extensions, dialTargetsById),
        },
    ]

    await Promise.all(files.map(async (file) => {
        const target = path.join(targetDir, file.name)
        await fs.writeFile(target, `${file.content}\n`, { mode: 0o644 })
        await fs.chmod(target, 0o644)
    }))

    return files.map((file) => path.join(targetDir, file.name))
}

const runAsteriskAmiCommand = async (command: string, timeoutMs = 5000) => {
    const config = asteriskAmiConfig()
    if (!config.host) {
        throw new Error("Asterisk-AMI ist nicht konfiguriert.")
    }

    return await new Promise<{ command: string, ok: boolean, raw: string }>((resolve, reject) => {
        const socket = net.createConnection(config.port, config.host)
        let raw = ""
        let settled = false

        const finish = (ok: boolean) => {
            if (settled) return
            settled = true
            socket.destroy()
            resolve({ command, ok, raw })
        }

        socket.setTimeout(timeoutMs)
        socket.on("connect", () => {
            socket.write([
                "Action: Login",
                `Username: ${config.username}`,
                `Secret: ${config.password}`,
                "Events: off",
                "",
                "Action: Command",
                `Command: ${command}`,
                "",
                "Action: Logoff",
                "",
            ].join("\r\n"))
        })
        socket.on("data", (chunk) => {
            raw += chunk.toString("utf8")
            if (raw.includes("Goodbye")) finish(!raw.includes("Authentication failed"))
        })
        socket.on("timeout", () => finish(raw.length > 0))
        socket.on("end", () => finish(!raw.includes("Authentication failed")))
        socket.on("error", reject)
    })
}

const runAsteriskReload = async (trunk: any) => {
    const provider = trunkProviderConfig(trunk?.provider)
    const commands = [
        "module reload res_pjsip.so",
        "dialplan reload",
        `pjsip send register ${provider.key}-registration`,
    ]
    const results = []

    for (const command of commands) {
        results.push(await runAsteriskAmiCommand(command))
    }

    return {
        ok: results.every((result) => result.ok),
        commands: results,
    }
}

const readAsteriskTrunkStatus = async (trunk?: any) => {
    const provider = trunkProviderConfig(trunk?.provider)
    const registrationName = `${provider.key}-registration`
    const registrations = await runAsteriskAmiCommand("pjsip show registrations")
    const raw = registrations.raw || ""

    return {
        reachable: registrations.ok,
        provider: provider.key,
        registered: new RegExp(`${registrationName}[\\s\\S]*Registered`, "i").test(raw),
        hasRegistration: raw.includes(registrationName),
        registrations: raw,
    }
}

export default async function telephonyRoutes(server: FastifyInstance) {
    const loadTenantTrunk = async (tenantId: number | null, provider = "telekom") => {
        if (!tenantId) return null

        const [trunk] = await server.db
            .select()
            .from(telephonyTrunks)
            .where(and(
                eq(telephonyTrunks.tenantId, tenantId),
                eq(telephonyTrunks.provider, provider)
            ))
            .limit(1)

        return trunk || null
    }

    const loadActiveTenantTrunk = async (tenantId: number | null) => {
        if (!tenantId) return null

        const trunks = await server.db
            .select()
            .from(telephonyTrunks)
            .where(eq(telephonyTrunks.tenantId, tenantId))

        return trunks.find((trunk) => trunk.enabled)
            || trunks.find((trunk) => trunk.provider === "easybell")
            || trunks.find((trunk) => trunk.provider === "telekom")
            || trunks[0]
            || null
    }

    const loadTenantExtensions = async (tenantId: number | null) => {
        if (!tenantId) return []

        return await server.db
            .select()
            .from(telephonyExtensions)
            .where(eq(telephonyExtensions.tenantId, tenantId))
    }

    const loadExtensionDialTargets = async (tenantId: number, extensions: any[]) => {
        const targetsById = new Map<string, string[]>()
        const userExtensionByUserId = new Map<string, string>()

        for (const extension of extensions) {
            if (!extension.enabled) continue

            if (extension.targetType === "user" && extension.targetUserId) {
                userExtensionByUserId.set(extension.targetUserId, extension.extension)
                targetsById.set(extension.id, [extension.extension])
            }
        }

        const profileRows = await server.db
            .select({
                id: authProfiles.id,
                userId: authProfiles.user_id,
                branchId: authProfiles.branch_id,
            })
            .from(authProfiles)
            .where(and(
                eq(authProfiles.tenant_id, tenantId),
                eq(authProfiles.active, true)
            ))

        const profileUserByProfileId = new Map<string, string>()
        for (const profile of profileRows) {
            if (profile.id && profile.userId) {
                profileUserByProfileId.set(profile.id, profile.userId)
            }
        }

        const profileIds = profileRows.map((profile) => profile.id).filter(Boolean)
        const teamRows = profileIds.length
            ? await server.db
                .select()
                .from(authProfileTeams)
                .where(inArray(authProfileTeams.profile_id, profileIds))
            : []
        const branchRows = profileIds.length
            ? await server.db
                .select()
                .from(authProfileBranches)
                .where(inArray(authProfileBranches.profile_id, profileIds))
            : []

        for (const extension of extensions) {
            if (!extension.enabled || extension.targetType === "user") continue

            const memberUserIds = new Set<string>()

            if (extension.targetType === "team" && extension.targetTeamId) {
                for (const row of teamRows) {
                    if (row.team_id !== extension.targetTeamId) continue
                    const userId = profileUserByProfileId.get(row.profile_id)
                    if (userId) memberUserIds.add(userId)
                }
            }

            if (extension.targetType === "branch" && extension.targetBranchId) {
                for (const profile of profileRows) {
                    if (profile.branchId === extension.targetBranchId && profile.userId) {
                        memberUserIds.add(profile.userId)
                    }
                }
                for (const row of branchRows) {
                    if (row.branch_id !== extension.targetBranchId) continue
                    const userId = profileUserByProfileId.get(row.profile_id)
                    if (userId) memberUserIds.add(userId)
                }
            }

            const dialTargets = Array.from(memberUserIds)
                .map((userId) => userExtensionByUserId.get(userId))
                .filter(Boolean) as string[]

            targetsById.set(extension.id, Array.from(new Set(dialTargets)))
        }

        return targetsById
    }

    const sanitizeExtension = (extension: any) => ({
        id: extension.id,
        tenantId: extension.tenantId,
        targetType: extension.targetType,
        targetUserId: extension.targetUserId,
        targetTeamId: extension.targetTeamId,
        targetBranchId: extension.targetBranchId,
        extension: extension.extension,
        displayName: extension.displayName || "",
        sipUsername: extension.sipUsername || extension.extension,
        sipPasswordConfigured: Boolean(extension.sipPassword),
        enabled: Boolean(extension.enabled),
        createdAt: extension.createdAt,
        updatedAt: extension.updatedAt,
    })

    const extensionTargetWhere = (tenantId: number, targetType: string, targetId: string | number | null) => {
        if (targetType === "team") {
            return and(
                eq(telephonyExtensions.tenantId, tenantId),
                eq(telephonyExtensions.targetType, "team"),
                eq(telephonyExtensions.targetTeamId, Number(targetId))
            )
        }

        if (targetType === "branch") {
            return and(
                eq(telephonyExtensions.tenantId, tenantId),
                eq(telephonyExtensions.targetType, "branch"),
                eq(telephonyExtensions.targetBranchId, Number(targetId))
            )
        }

        return and(
            eq(telephonyExtensions.tenantId, tenantId),
            eq(telephonyExtensions.targetType, "user"),
            eq(telephonyExtensions.targetUserId, String(targetId))
        )
    }

    const resolveExtensionTargetId = (targetType: string, body: any) => {
        if (targetType === "team") return bodyNumber(body, "targetTeamId") ?? bodyNumber(body, "targetId")
        if (targetType === "branch") return bodyNumber(body, "targetBranchId") ?? bodyNumber(body, "targetId")
        return bodyString(body, "targetUserId") || bodyString(body, "targetId")
    }

    const validateExtensionDuplicate = async (
        tenantId: number,
        extension: string,
        currentExtensionId: string | null = null
    ) => {
        const conditions = [
            eq(telephonyExtensions.tenantId, tenantId),
            eq(telephonyExtensions.extension, extension),
        ]

        if (currentExtensionId) {
            conditions.push(ne(telephonyExtensions.id, currentExtensionId))
        }

        const existing = await server.db
            .select({ id: telephonyExtensions.id })
            .from(telephonyExtensions)
            .where(and(...conditions))
            .limit(1)

        return existing[0] ? "Diese Nebenstelle ist bereits vergeben." : null
    }

    const externalTelephonyConfig = async (tenantId: number | null) => {
        const trunk = await loadActiveTenantTrunk(tenantId)
        if (trunk) {
            return {
                enabled: trunk.enabled,
                provider: trunk.provider,
                inboundExtension: trunk.inboundExtension,
                defaultRouteExtensionId: trunk.defaultRouteExtensionId,
                outboundPrefix: trunk.outboundPrefix,
                registrar: trunk.registrar,
                externalSignalingAddress: trunk.externalSignalingAddress,
                externalMediaAddress: trunk.externalMediaAddress,
                localNetworks: trunk.localNetworks,
                sipUserConfigured: Boolean(trunk.sipUser),
                authUserConfigured: Boolean(trunk.authUser),
                passwordConfigured: Boolean(trunk.password),
                callerIdConfigured: Boolean(trunk.callerId),
            }
        }

        return envExternalTelephonyConfig()
    }

    const loadUserSipAccounts = async (tenantId: number | null, userId: string | null | undefined) => {
        if (!tenantId || !userId) return []

        const extensions = await server.db
            .select()
            .from(telephonyExtensions)
            .where(and(
                eq(telephonyExtensions.tenantId, tenantId),
                eq(telephonyExtensions.targetType, "user"),
                eq(telephonyExtensions.targetUserId, userId),
                eq(telephonyExtensions.enabled, true)
            ))

        return extensions.map((extension) => ({
            extension: extension.extension,
            password: extension.sipPassword,
            displayName: extension.displayName || `Nebenstelle ${extension.extension}`,
        })).filter((account) => account.extension && account.password)
    }

    server.get("/telephony/config", async (req) => ({
            enabled: telephonyEnabled(),
            provider: "asterisk",
            mode: "asterisk",
            sipDomain: sipDomain(),
            sipWebSocketUrl: publicAsteriskWsUrl(),
            echoExtension: process.env.TELEPHONY_ECHO_EXTENSION || "600",
            accounts: await loadUserSipAccounts(req.user?.tenant_id || null, req.user?.user_id),
            external: await externalTelephonyConfig(req.user?.tenant_id || null),
        }))

    server.get("/telephony/status", async () => {
        const enabled = telephonyEnabled()
        const urls = asteriskHttpStatusUrls()

        let lastError: any = null
        const attempts = []

        for (const url of urls) {
            try {
                const response = await fetchWithTimeout(url)
                attempts.push({
                    url,
                    reachable: true,
                    statusCode: response.status,
                })

                return {
                    enabled,
                    provider: "asterisk",
                    reachable: true,
                    statusCode: response.status,
                    statusUrl: url,
                    attempts,
                    message: enabled
                        ? (response.ok ? "Asterisk ist erreichbar." : `Asterisk-HTTP ist erreichbar (HTTP ${response.status}).`)
                        : "Asterisk ist erreichbar, Telefonie ist aber noch nicht aktiviert.",
                }
            } catch (error: any) {
                lastError = error
                attempts.push({
                    url,
                    reachable: false,
                    message: error?.name === "AbortError"
                        ? "Abgelaufen"
                        : (error?.message || "Nicht erreichbar"),
                })
            }
        }

        return {
            enabled,
            provider: "asterisk",
            reachable: false,
            statusUrl: urls[0],
            attempts,
            message: !urls.length
                ? "Asterisk-Status-URL ist nicht konfiguriert."
                : lastError?.name === "AbortError"
                ? "Asterisk-Statusabfrage ist abgelaufen."
                : (lastError?.message || "Asterisk ist nicht erreichbar."),
        }
    })

    server.get("/telephony/trunk-config", async (req) => {
        const tenantId = requireTenant(req.user.tenant_id)
        const provider = normalizeTrunkProvider((req.query as { provider?: string })?.provider)
        const trunk = await loadTenantTrunk(tenantId, provider)

        return sanitizeTrunk(trunk || { provider })
    })

    server.put("/telephony/trunk-config", async (req, reply) => {
        const tenantId = requireTenant(req.user.tenant_id)
        const body = (req.body || {}) as any
        const provider = normalizeTrunkProvider(bodyString(body, "provider"))
        const providerConfig = trunkProviderConfig(provider)
        const existing = await loadTenantTrunk(tenantId, provider)
        const password = bodyString(body, "password")
        const clearPassword = body?.clearPassword === true
        const now = new Date()
        const values = {
            tenantId,
            provider,
            enabled: Boolean(body.enabled),
            registrar: bodyString(body, "registrar") || providerConfig.defaultRegistrar,
            sipUser: bodyString(body, "sipUser"),
            authUser: bodyString(body, "authUser"),
            callerId: bodyString(body, "callerId"),
            inboundExtension: bodyString(body, "inboundExtension") || "1001",
            defaultRouteExtensionId: bodyString(body, "defaultRouteExtensionId"),
            outboundPrefix: bodyString(body, "outboundPrefix") || "0",
            externalSignalingAddress: bodyString(body, "externalSignalingAddress"),
            externalMediaAddress: bodyString(body, "externalMediaAddress"),
            localNetworks: bodyString(body, "localNetworks") || "172.16.0.0/12,192.168.0.0/16,10.0.0.0/8",
            password: clearPassword ? null : (password || existing?.password || null),
            updatedAt: now,
            updatedBy: req.user.user_id,
        }

        if (values.enabled && (!values.sipUser || !values.password)) {
            return reply.code(400).send({
                error: "SIP-ID und Kennwort sind erforderlich, wenn der Trunk aktiviert wird.",
            })
        }

        if (existing) {
            const [updated] = await server.db
                .update(telephonyTrunks)
                .set(values)
                .where(and(
                    eq(telephonyTrunks.tenantId, tenantId),
                    eq(telephonyTrunks.provider, provider)
                ))
                .returning()

            if (values.enabled) {
                await server.db
                    .update(telephonyTrunks)
                    .set({ enabled: false, updatedAt: now, updatedBy: req.user.user_id })
                    .where(and(
                        eq(telephonyTrunks.tenantId, tenantId),
                        eq(telephonyTrunks.enabled, true),
                        eq(telephonyTrunks.provider, provider === "easybell" ? "telekom" : "easybell")
                    ))
            }

            return sanitizeTrunk(updated)
        }

        const [created] = await server.db
            .insert(telephonyTrunks)
            .values({
                ...values,
                createdAt: now,
                createdBy: req.user.user_id,
            })
            .returning()

        if (values.enabled) {
            await server.db
                .update(telephonyTrunks)
                .set({ enabled: false, updatedAt: now, updatedBy: req.user.user_id })
                .where(and(
                    eq(telephonyTrunks.tenantId, tenantId),
                    eq(telephonyTrunks.enabled, true),
                    eq(telephonyTrunks.provider, provider === "easybell" ? "telekom" : "easybell")
                ))
        }

        return sanitizeTrunk(created)
    })

    server.post("/telephony/trunk-config/apply", async (req, reply) => {
        const tenantId = requireTenant(req.user.tenant_id)
        const trunk = await loadActiveTenantTrunk(tenantId)

        if (!trunk) {
            return reply.code(404).send({ error: "Telefonie-Trunk ist noch nicht konfiguriert." })
        }

        if (trunk.enabled && (!trunk.sipUser || !trunk.password)) {
            return reply.code(400).send({
                error: "SIP-ID und Kennwort sind erforderlich, bevor der Trunk angewendet werden kann.",
            })
        }

        const extensions = await loadTenantExtensions(tenantId)
        const dialTargetsById = await loadExtensionDialTargets(tenantId, extensions)
        const defaultRoute = extensions.find((extension) => extension.id === trunk.defaultRouteExtensionId)
        const files = await writeAsteriskTrunkConfig({
            ...trunk,
            inboundExtension: defaultRoute?.extension || trunk.inboundExtension,
        }, extensions, dialTargetsById)
        let reload: any = null
        let status: any = null
        let warning: string | null = null

        try {
            reload = await runAsteriskReload(trunk)
            status = await readAsteriskTrunkStatus(trunk)
        } catch (error: any) {
            warning = error?.message || "Asterisk konnte nicht neu geladen werden."
        }

        return {
            generated: true,
            files,
            trunk: sanitizeTrunk(trunk),
            reload,
            status,
            warning,
        }
    })

    server.get("/telephony/trunk-status", async (req, reply) => {
        const tenantId = requireTenant(req.user.tenant_id)
        const trunk = await loadActiveTenantTrunk(tenantId)

        try {
            return await readAsteriskTrunkStatus(trunk)
        } catch (error: any) {
            return reply.code(200).send({
                reachable: false,
                registered: false,
                hasRegistration: false,
                registrations: "",
                message: error?.message || "Asterisk-AMI ist nicht erreichbar.",
            })
        }
    })

    server.get("/telephony/extensions/options", async (req) => {
        const tenantId = requireTenant(req.user.tenant_id)

        const tenantUsers = await server.db
            .select({
                id: authUsers.id,
                email: authUsers.email,
                firstName: authProfiles.first_name,
                lastName: authProfiles.last_name,
            })
            .from(authProfiles)
            .innerJoin(authUsers, eq(authUsers.id, authProfiles.user_id))
            .where(and(
                eq(authProfiles.tenant_id, tenantId),
                eq(authProfiles.active, true)
            ))

        const tenantTeams = await server.db
            .select({
                id: teams.id,
                name: teams.name,
            })
            .from(teams)
            .where(and(
                eq(teams.tenant, tenantId),
                eq(teams.archived, false)
            ))

        const tenantBranches = await server.db
            .select({
                id: branches.id,
                name: branches.name,
                number: branches.number,
            })
            .from(branches)
            .where(and(
                eq(branches.tenant, tenantId),
                eq(branches.archived, false)
            ))

        return {
            users: tenantUsers.map((user) => ({
                id: user.id,
                label: [user.firstName, user.lastName].filter(Boolean).join(" ") || user.email,
                description: user.email,
            })),
            teams: tenantTeams.map((team) => ({
                id: team.id,
                label: team.name,
            })),
            branches: tenantBranches.map((branch) => ({
                id: branch.id,
                label: branch.number ? `${branch.number} - ${branch.name}` : branch.name,
            })),
        }
    })

    server.get("/telephony/extensions", async (req) => {
        const tenantId = requireTenant(req.user.tenant_id)
        const extensions = await loadTenantExtensions(tenantId)
        const dialTargetsById = await loadExtensionDialTargets(tenantId, extensions)
        const options = await (async () => {
            const tenantUsers = await server.db
                .select({
                    id: authUsers.id,
                    email: authUsers.email,
                    firstName: authProfiles.first_name,
                    lastName: authProfiles.last_name,
                })
                .from(authProfiles)
                .innerJoin(authUsers, eq(authUsers.id, authProfiles.user_id))
                .where(and(
                    eq(authProfiles.tenant_id, tenantId),
                    eq(authProfiles.active, true)
                ))

            const tenantTeams = await server.db
                .select({ id: teams.id, name: teams.name })
                .from(teams)
                .where(and(eq(teams.tenant, tenantId), eq(teams.archived, false)))

            const tenantBranches = await server.db
                .select({ id: branches.id, name: branches.name, number: branches.number })
                .from(branches)
                .where(and(eq(branches.tenant, tenantId), eq(branches.archived, false)))

            return {
                users: new Map(tenantUsers.map((user) => [
                    user.id,
                    [user.firstName, user.lastName].filter(Boolean).join(" ") || user.email,
                ])),
                teams: new Map(tenantTeams.map((team) => [team.id, team.name])),
                branches: new Map(tenantBranches.map((branch) => [
                    branch.id,
                    branch.number ? `${branch.number} - ${branch.name}` : branch.name,
                ])),
            }
        })()

        return {
            rows: extensions
                .sort((a, b) => a.extension.localeCompare(b.extension, "de"))
                .map((extension) => ({
                    ...sanitizeExtension(extension),
                    targetLabel: extension.targetType === "team"
                        ? options.teams.get(extension.targetTeamId)
                        : extension.targetType === "branch"
                            ? options.branches.get(extension.targetBranchId)
                            : options.users.get(extension.targetUserId),
                    dialTargets: dialTargetsById.get(extension.id) || [],
                })),
        }
    })

    server.get("/telephony/extensions/target", async (req) => {
        const tenantId = requireTenant(req.user.tenant_id)
        const query = req.query as { targetType?: string; targetId?: string }
        const targetType = normalizeExtensionTargetType(query.targetType)
        const targetId = targetType === "user" ? query.targetId : Number(query.targetId)

        if (!targetId || (targetType !== "user" && !Number.isFinite(Number(targetId)))) {
            return { extension: null }
        }

        const existing = await server.db
            .select()
            .from(telephonyExtensions)
            .where(extensionTargetWhere(tenantId, targetType, targetId))
            .limit(1)

        return { extension: existing[0] ? sanitizeExtension(existing[0]) : null }
    })

    server.put("/telephony/extensions/target", async (req, reply) => {
        const tenantId = requireTenant(req.user.tenant_id)
        const body = (req.body || {}) as any
        const targetType = normalizeExtensionTargetType(bodyString(body, "targetType"))
        const targetId = resolveExtensionTargetId(targetType, body)
        const extension = normalizeExtension(bodyString(body, "extension"))

        if (!targetId) {
            return reply.code(400).send({ error: "Ziel der Nebenstelle ist erforderlich." })
        }

        if (!extension) {
            return reply.code(400).send({ error: "Nebenstelle ist erforderlich." })
        }

        const existing = await server.db
            .select()
            .from(telephonyExtensions)
            .where(extensionTargetWhere(tenantId, targetType, targetId))
            .limit(1)

        const duplicateError = await validateExtensionDuplicate(tenantId, extension, existing[0]?.id || null)
        if (duplicateError) {
            return reply.code(409).send({ error: duplicateError })
        }

        const displayName = bodyString(body, "displayName")
        const now = new Date()

        if (existing[0]) {
            const [updated] = await server.db
                .update(telephonyExtensions)
                .set({
                    extension,
                    displayName,
                    sipUsername: extension,
                    enabled: body?.enabled !== false,
                    updatedAt: now,
                    updatedBy: req.user.user_id,
                })
                .where(and(
                    eq(telephonyExtensions.tenantId, tenantId),
                    eq(telephonyExtensions.id, existing[0].id)
                ))
                .returning()

            return sanitizeExtension(updated)
        }

        const [created] = await server.db
            .insert(telephonyExtensions)
            .values({
                tenantId,
                targetType,
                targetUserId: targetType === "user" ? String(targetId) : null,
                targetTeamId: targetType === "team" ? Number(targetId) : null,
                targetBranchId: targetType === "branch" ? Number(targetId) : null,
                extension,
                displayName,
                sipUsername: extension,
                sipPassword: generateSipPassword(),
                enabled: body?.enabled !== false,
                createdBy: req.user.user_id,
                updatedAt: now,
                updatedBy: req.user.user_id,
            })
            .returning()

        return reply.code(201).send(sanitizeExtension(created))
    })

    server.delete("/telephony/extensions/target", async (req, reply) => {
        const tenantId = requireTenant(req.user.tenant_id)
        const query = req.query as { targetType?: string; targetId?: string }
        const targetType = normalizeExtensionTargetType(query.targetType)
        const targetId = targetType === "user" ? query.targetId : Number(query.targetId)

        if (!targetId || (targetType !== "user" && !Number.isFinite(Number(targetId)))) {
            return reply.code(400).send({ error: "Ziel der Nebenstelle ist erforderlich." })
        }

        await server.db
            .delete(telephonyExtensions)
            .where(extensionTargetWhere(tenantId, targetType, targetId))

        return { deleted: true }
    })

    server.post("/telephony/extensions", async (req, reply) => {
        const tenantId = requireTenant(req.user.tenant_id)
        const body = (req.body || {}) as any
        const targetType = normalizeExtensionTargetType(bodyString(body, "targetType"))
        const extension = normalizeExtension(bodyString(body, "extension"))

        if (!extension) {
            return reply.code(400).send({ error: "Nebenstelle ist erforderlich." })
        }

        const duplicateError = await validateExtensionDuplicate(tenantId, extension)
        if (duplicateError) {
            return reply.code(409).send({ error: duplicateError })
        }

        const targetUserId = targetType === "user" ? bodyString(body, "targetUserId") : null
        const targetTeamId = targetType === "team" ? bodyNumber(body, "targetTeamId") : null
        const targetBranchId = targetType === "branch" ? bodyNumber(body, "targetBranchId") : null

        if (
            (targetType === "user" && !targetUserId)
            || (targetType === "team" && !targetTeamId)
            || (targetType === "branch" && !targetBranchId)
        ) {
            return reply.code(400).send({ error: "Ziel der Nebenstelle ist erforderlich." })
        }

        const sipPassword = bodyString(body, "sipPassword") || generateSipPassword()
        const now = new Date()
        const [created] = await server.db
            .insert(telephonyExtensions)
            .values({
                tenantId,
                targetType,
                targetUserId,
                targetTeamId,
                targetBranchId,
                extension,
                displayName: bodyString(body, "displayName"),
                sipUsername: bodyString(body, "sipUsername") || extension,
                sipPassword,
                enabled: body?.enabled !== false,
                createdBy: req.user.user_id,
                updatedAt: now,
                updatedBy: req.user.user_id,
            })
            .returning()

        return reply.code(201).send(sanitizeExtension(created))
    })

    server.put("/telephony/extensions/:id", async (req, reply) => {
        const tenantId = requireTenant(req.user.tenant_id)
        const params = req.params as { id: string }
        const body = (req.body || {}) as any
        const targetType = normalizeExtensionTargetType(bodyString(body, "targetType"))
        const extension = normalizeExtension(bodyString(body, "extension"))

        if (!extension) {
            return reply.code(400).send({ error: "Nebenstelle ist erforderlich." })
        }

        const existing = await server.db
            .select()
            .from(telephonyExtensions)
            .where(and(
                eq(telephonyExtensions.tenantId, tenantId),
                eq(telephonyExtensions.id, params.id)
            ))
            .limit(1)

        if (!existing[0]) {
            return reply.code(404).send({ error: "Nebenstelle nicht gefunden." })
        }

        const duplicateError = await validateExtensionDuplicate(tenantId, extension, params.id)
        if (duplicateError) {
            return reply.code(409).send({ error: duplicateError })
        }

        const sipPassword = bodyString(body, "sipPassword")
        const [updated] = await server.db
            .update(telephonyExtensions)
            .set({
                targetType,
                targetUserId: targetType === "user" ? bodyString(body, "targetUserId") : null,
                targetTeamId: targetType === "team" ? bodyNumber(body, "targetTeamId") : null,
                targetBranchId: targetType === "branch" ? bodyNumber(body, "targetBranchId") : null,
                extension,
                displayName: bodyString(body, "displayName"),
                sipUsername: bodyString(body, "sipUsername") || extension,
                ...(sipPassword ? { sipPassword } : {}),
                enabled: body?.enabled !== false,
                updatedAt: new Date(),
                updatedBy: req.user.user_id,
            })
            .where(and(
                eq(telephonyExtensions.tenantId, tenantId),
                eq(telephonyExtensions.id, params.id)
            ))
            .returning()

        return sanitizeExtension(updated)
    })

    server.delete("/telephony/extensions/:id", async (req, reply) => {
        const tenantId = requireTenant(req.user.tenant_id)
        const params = req.params as { id: string }

        const deleted = await server.db
            .delete(telephonyExtensions)
            .where(and(
                eq(telephonyExtensions.tenantId, tenantId),
                eq(telephonyExtensions.id, params.id)
            ))
            .returning()

        if (!deleted[0]) {
            return reply.code(404).send({ error: "Nebenstelle nicht gefunden." })
        }

        return { deleted: true }
    })

    server.get("/telephony/calls", async (req) => {
        const tenantId = requireTenant(req.user.tenant_id)
        const query = req.query as {
            limit?: string
            direction?: string
            status?: string
            search?: string
            localExtension?: string
        }
        const limit = Math.min(
            Math.max(Number(query?.limit || 25), 1),
            100
        )
        const filters = [eq(telephonyCalls.tenantId, tenantId)]

        if (query.direction === "incoming" || query.direction === "outgoing") {
            filters.push(eq(telephonyCalls.direction, query.direction))
        }

        if (query.status && query.status !== "all") {
            filters.push(eq(telephonyCalls.status, query.status))
        }

        if (query.localExtension) {
            filters.push(eq(telephonyCalls.localExtension, query.localExtension))
        }

        if (query.search?.trim()) {
            const search = `%${query.search.trim()}%`
            filters.push(or(
                ilike(telephonyCalls.remoteNumber, search),
                ilike(telephonyCalls.remoteDisplayName, search),
                ilike(telephonyCalls.localExtension, search)
            )!)
        }

        return await server.db
            .select()
            .from(telephonyCalls)
            .where(and(...filters))
            .orderBy(desc(telephonyCalls.startedAt))
            .limit(limit)
    })

    server.post("/telephony/calls", async (req, reply) => {
        const tenantId = requireTenant(req.user.tenant_id)
        const body = (req.body || {}) as any
        const now = new Date()
        const startedAt = bodyDate(body, "startedAt") || now
        const direction = bodyString(body, "direction") === "incoming" ? "incoming" : "outgoing"
        const status = bodyString(body, "status") || (direction === "incoming" ? "ringing" : "dialing")

        const [created] = await server.db
            .insert(telephonyCalls)
            .values({
                tenantId,
                direction,
                status,
                localExtension: bodyString(body, "localExtension"),
                remoteNumber: bodyString(body, "remoteNumber"),
                remoteDisplayName: bodyString(body, "remoteDisplayName"),
                sipCallId: bodyString(body, "sipCallId"),
                startedAt,
                createdBy: req.user.user_id,
            })
            .returning()

        return reply.code(201).send(created)
    })

    server.patch("/telephony/calls/:id", async (req, reply) => {
        const tenantId = requireTenant(req.user.tenant_id)
        const params = req.params as { id: string }
        const body = (req.body || {}) as any

        const [existing] = await server.db
            .select()
            .from(telephonyCalls)
            .where(and(
                eq(telephonyCalls.tenantId, tenantId),
                eq(telephonyCalls.id, params.id)
            ))
            .limit(1)

        if (!existing) {
            return reply.code(404).send({ error: "Anruf nicht gefunden" })
        }

        const answeredAt = bodyDate(body, "answeredAt")
        const endedAt = bodyDate(body, "endedAt")
        const startedAt = existing.startedAt ? new Date(existing.startedAt) : null
        const computedDuration = endedAt
            ? durationSeconds(answeredAt || startedAt, endedAt)
            : null

        const [updated] = await server.db
            .update(telephonyCalls)
            .set({
                status: bodyString(body, "status") || existing.status,
                localExtension: bodyString(body, "localExtension") || existing.localExtension,
                remoteNumber: bodyString(body, "remoteNumber") || existing.remoteNumber,
                remoteDisplayName: bodyString(body, "remoteDisplayName") || existing.remoteDisplayName,
                sipCallId: bodyString(body, "sipCallId") || existing.sipCallId,
                answeredAt: answeredAt || existing.answeredAt,
                endedAt: endedAt || existing.endedAt,
                durationSeconds: computedDuration ?? existing.durationSeconds,
                updatedAt: new Date(),
                updatedBy: req.user.user_id,
            })
            .where(and(
                eq(telephonyCalls.tenantId, tenantId),
                eq(telephonyCalls.id, params.id)
            ))
            .returning()

        return updated
    })
}
