import { eq } from "drizzle-orm"
import { db } from "../db/index.js"
import { servers } from "../db/schema.js"
import type { TrafficFlowOverlayResult } from "@mmapp/contracts/traffic-flow"
import { encodeRosId, MikrotikClient, MikrotikError } from "./mikrotik.js"
import { getEnabledServerById, listWireGuardInterfaces } from "./wireguard-live.js"
import {
  asRosArray,
  patchRosPath,
  putIpAddress,
  putWireguardInterface,
  putWireguardPeer,
  rosRowId,
  toRosBody,
} from "./wireguard-ros.js"
import {
  enableTrafficFlowIngest,
  ensureHostKeys,
  getTrafficFlowSettingsRow,
  upsertHostPeer,
} from "./traffic-flow-settings.js"
import { refreshFlowExporterMap, startTrafficFlowListener } from "./traffic-flow-ingest.js"
import { listTrafficFlowHostFiles } from "./traffic-flow-host-files.js"

const IFACE_NAME = "wg-flow"
const JH_LISTEN_PORT = 13232
const WG_INPUT_COMMENT = "mm-wg-flow"

export function allocateOverlayAddress(prefix: string, collectorIp: string, serverId: number, taken: Set<string>): string {
  const [base] = prefix.split("/")
  const parts = (base ?? "10.255.254.0").split(".").map((n) => Number.parseInt(n, 10))
  const a = parts[0] || 10
  const b = parts[1] || 255
  const c = parts[2] || 254
  const preferredLast = 2 + ((serverId - 1) % 250)
  const candidates = [preferredLast, ...Array.from({ length: 253 }, (_, i) => 2 + ((preferredLast - 2 + i) % 253))]
  for (const last of candidates) {
    const ip = `${a}.${b}.${c}.${last}`
    if (ip === collectorIp) continue
    if (taken.has(ip)) continue
    return ip
  }
  throw new Error("Нет свободных адресов в префиксе wg-flow")
}

function linuxPeerBlock(publicKey: string, address: string, comment: string, endpoint: string): string {
  return [
    `[Peer]`,
    `PublicKey = ${publicKey}`,
    `AllowedIPs = ${address}/32`,
    `Endpoint = ${endpoint}:${JH_LISTEN_PORT}`,
    `PersistentKeepalive = 25`,
    comment ? `# ${comment}` : "",
  ].filter(Boolean).join("\n")
}

async function findIface(client: MikrotikClient, name: string): Promise<Record<string, unknown> | undefined> {
  const list = asRosArray<Record<string, unknown>>(await client.get("/interface/wireguard"))
  return list.find((i) => String(i.name ?? "") === name)
}

async function findPeer(
  client: MikrotikClient,
  iface: string,
  publicKey: string,
): Promise<Record<string, unknown> | undefined> {
  const list = asRosArray<Record<string, unknown>>(await client.get("/interface/wireguard/peers"))
  return list.find((p) =>
    String(p.interface ?? "") === iface && String(p["public-key"] ?? "") === publicKey,
  )
}

async function findAddress(client: MikrotikClient, iface: string): Promise<Record<string, unknown> | undefined> {
  const list = asRosArray<Record<string, unknown>>(await client.get("/ip/address"))
  return list.find((a) => String(a.interface ?? "") === iface)
}

async function findRoute(client: MikrotikClient, dst: string): Promise<Record<string, unknown> | undefined> {
  const list = asRosArray<Record<string, unknown>>(await client.get("/ip/route"))
  return list.find((r) => String(r["dst-address"] ?? "") === dst)
}

async function ensureWgInputAccept(client: MikrotikClient, listenPort: number): Promise<boolean> {
  const rules = asRosArray<Record<string, unknown>>(await client.get("/ip/firewall/filter"))
  const existing = rules.find((r) => String(r.comment ?? "") === WG_INPUT_COMMENT)
  if (existing) return false
  await client.put("/ip/firewall/filter", toRosBody({
    chain: "input",
    protocol: "udp",
    "dst-port": String(listenPort),
    action: "accept",
    comment: WG_INPUT_COMMENT,
  }))
  return true
}

/** Официальный авто-source UDP IPFIX, не фильтр 0.0.0.0/0. */
export const FLOW_TARGET_SRC_AUTO = "0.0.0.0"

async function ensureIpfixFields(client: MikrotikClient): Promise<void> {
  const body = toRosBody({
    bytes: "yes",
    packets: "yes",
    "src-address": "yes",
    "dst-address": "yes",
    protocol: "yes",
    "src-port": "yes",
    "dst-port": "yes",
    "in-interface": "yes",
    "out-interface": "yes",
    gateway: "yes",
    "first-forwarded": "yes",
    "last-forwarded": "yes",
    "nat-src-address": "yes",
    "nat-dst-address": "yes",
  })
  const rows = asRosArray<Record<string, unknown>>(await client.get("/ip/traffic-flow/ipfix"))
  const id = rows[0] ? rosRowId(rows[0]) : ""
  if (id) {
    await patchRosPath(client, `/ip/traffic-flow/ipfix/${encodeRosId(id)}`, body)
    return
  }
  await client.post("/ip/traffic-flow/ipfix/set", body)
}

async function ensureTrafficFlow(
  client: MikrotikClient,
  collectorIp: string,
  port: number,
): Promise<void> {
  const body = toRosBody({
    enabled: "yes",
    interfaces: "all",
    "active-flow-timeout": "1m",
    "inactive-flow-timeout": "15s",
  })
  const rows = asRosArray<Record<string, unknown>>(await client.get("/ip/traffic-flow"))
  const id = rows[0] ? rosRowId(rows[0]) : ""
  if (id) {
    await patchRosPath(client, `/ip/traffic-flow/${encodeRosId(id)}`, body)
  } else {
    await client.post("/ip/traffic-flow/set", body)
  }

  try {
    await ensureIpfixFields(client)
  } catch {
    /* поля IPFIX опциональны на старых ROS */
  }

  const targets = asRosArray<Record<string, unknown>>(await client.get("/ip/traffic-flow/target"))
  const existing = targets.find((t) => String(t["dst-address"] ?? "") === collectorIp)
  const targetBody = toRosBody({
    "dst-address": collectorIp,
    "src-address": FLOW_TARGET_SRC_AUTO,
    port: String(port),
    version: "ipfix",
  })
  if (existing) {
    const targetId = rosRowId(existing)
    if (targetId) await patchRosPath(client, `/ip/traffic-flow/target/${encodeRosId(targetId)}`, targetBody)
    return
  }
  await client.put("/ip/traffic-flow/target", targetBody)
}

export function usablePublicHost(raw: string | undefined): string {
  if (!raw) return ""
  const host = raw.split(",")[0]?.trim().replace(/^\[/, "").replace(/\]:\d+$/, "").split(":")[0]?.trim() ?? ""
  const lower = host.toLowerCase()
  if (!host) return ""
  if (lower === "localhost" || lower === "127.0.0.1" || lower === "::1" || lower === "0.0.0.0") return ""
  if (lower.endsWith(".local") || lower.endsWith(".internal") || lower.endsWith(".lan")) return ""
  if (/^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(host)) return ""
  return host
}

export async function applyFlowOverlay(
  serverIdRaw: string | number,
  opts?: { publicEndpoint?: string; requestHost?: string },
): Promise<TrafficFlowOverlayResult> {
  const steps: string[] = []
  const keys = ensureHostKeys()
  const settings = getTrafficFlowSettingsRow()
  const hostPublicKey = settings.hostPublicKey || keys.publicKey
  if (!hostPublicKey) {
    throw Object.assign(new Error("Не удалось создать ключи хоста MM"), { statusCode: 500 })
  }

  const server = getEnabledServerById(String(serverIdRaw))
  if (!server || !server.enabled) {
    throw Object.assign(new Error("Сервер не найден или выключен"), { statusCode: 404 })
  }

  const endpointHost = (opts?.publicEndpoint?.trim() || server.host.trim()).trim()
  if (!endpointHost) {
    throw Object.assign(new Error("Укажите публичный IP или DNS jump-host"), { statusCode: 400 })
  }
  const peerEndpoint = `${endpointHost}:${JH_LISTEN_PORT}`

  const taken = new Set(
    db.select({ ip: servers.mgmtTunnelIp }).from(servers).all()
      .map((r) => r.ip)
      .filter(Boolean),
  )
  const address = server.mgmtTunnelIp || allocateOverlayAddress(settings.prefix, settings.collectorIp, server.id, taken)
  const client = MikrotikClient.fromServer(server)

  try {
    let iface = await findIface(client, IFACE_NAME)
    if (!iface) {
      await putWireguardInterface(client, {
        name: IFACE_NAME,
        "listen-port": String(JH_LISTEN_PORT),
        mtu: "1420",
        comment: "MikrotikManager traffic-flow overlay",
      })
      steps.push(`Создан интерфейс ${IFACE_NAME}`)
      iface = await findIface(client, IFACE_NAME)
    } else {
      steps.push(`Интерфейс ${IFACE_NAME} уже есть`)
    }

    const addrRow = await findAddress(client, IFACE_NAME)
    const mask = (settings.prefix.split("/")[1] || "24").replace(/\D/g, "") || "24"
    const cidr = `${address}/${mask}`
    if (!addrRow) {
      await putIpAddress(client, cidr, IFACE_NAME)
      steps.push(`Адрес ${cidr}`)
    } else {
      steps.push(`Адрес на ${IFACE_NAME} уже назначен`)
    }

    const peer = await findPeer(client, IFACE_NAME, hostPublicKey)
    const peerBody = {
      interface: IFACE_NAME,
      "public-key": hostPublicKey,
      "allowed-address": `${settings.collectorIp}/32`,
      comment: "MM traffic-flow collector",
      name: "mm-collector",
    }
    if (!peer) {
      await putWireguardPeer(client, peerBody)
      steps.push("Добавлен пир на pubkey хоста MM (сервер, без endpoint)")
    } else {
      const id = rosRowId(peer)
      const hadEndpoint = Boolean(String(peer["endpoint-address"] ?? "").trim())
      if (hadEndpoint && id) {
        await client.delete(`/interface/wireguard/peers/${encodeURIComponent(id)}`)
        await putWireguardPeer(client, peerBody)
        steps.push("Пир пересоздан как сервер (endpoint снят)")
      } else if (id) {
        await patchRosPath(client, `/interface/wireguard/peers/${encodeURIComponent(id)}`, peerBody)
        steps.push("Пир хоста MM обновлён")
      }
    }

    const routeDst = `${settings.collectorIp}/32`
    const route = await findRoute(client, routeDst)
    if (!route) {
      await client.put("/ip/route", toRosBody({
        "dst-address": routeDst,
        gateway: IFACE_NAME,
        comment: "MM traffic-flow collector",
      }))
      steps.push(`Маршрут ${routeDst} через ${IFACE_NAME}`)
    } else {
      steps.push("Маршрут до collector уже есть")
    }

    if (await ensureWgInputAccept(client, JH_LISTEN_PORT)) {
      steps.push(`Firewall input accept UDP ${JH_LISTEN_PORT}`)
    } else {
      steps.push("Firewall input WG уже есть")
    }

    await ensureTrafficFlow(client, settings.collectorIp, settings.flowListenPort)
    steps.push(`Traffic Flow → ${settings.collectorIp}:${settings.flowListenPort} ipfix (src auto)`)

    const listed = await listWireGuardInterfaces({ serverId: String(server.id), includePrivateKey: false })
    const created = listed.interfaces.find((i) => i.name === IFACE_NAME)
    const publicKey = created?.publicKey ?? ""
    if (!publicKey) {
      throw new Error("Не удалось прочитать public-key интерфейса wg-flow")
    }

    db.update(servers).set({
      mgmtTunnelIp: address,
      updatedAt: new Date().toISOString(),
    }).where(eq(servers.id, server.id)).run()

    upsertHostPeer({
      serverId: server.id,
      name: server.name || server.host,
      publicKey,
      allowedIps: [`${address}/32`],
      address,
      endpoint: peerEndpoint,
    })

    enableTrafficFlowIngest()
    startTrafficFlowListener()
    refreshFlowExporterMap()
    steps.push("Коллектор IPFIX на MM включён")

    return {
      ok: true,
      serverId: server.id,
      interfaceName: IFACE_NAME,
      address,
      publicKey,
      linuxPeerBlock: linuxPeerBlock(publicKey, address, server.name || server.host, endpointHost),
      trafficFlow: true,
      steps,
      hostFiles: listTrafficFlowHostFiles(),
    }
  } catch (e) {
    const msg = e instanceof MikrotikError ? e.message : e instanceof Error ? e.message : String(e)
    const err = Object.assign(new Error(`RouterOS: ${msg}`), { statusCode: 502 })
    throw err
  }
}
