import { eq, sql } from "drizzle-orm"
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { z } from "zod"
import { db } from "../db/index.js"
import { evobgpSettings, filterRules } from "../db/schema.js"

const PutEvobgpSchema = z.object({
  baseUrl: z.string().optional(),
  enabled: z.boolean().optional(),
  /** undefined — не менять; null или "" — очистить ключ; строка — новое значение */
  apiKey: z.union([z.string(), z.null()]).optional(),
})

/** Опционально: проверить черновики до сохранения; поле отсутствует — взять из БД */
const TestEvobgpSchema = z.object({
  baseUrl: z.string().optional(),
  apiKey: z.string().optional(),
})

function ensureEvobgpRow() {
  let row = db.select().from(evobgpSettings).where(eq(evobgpSettings.id, 1)).limit(1).all()[0]
  if (!row) {
    db.insert(evobgpSettings).values({ id: 1 }).run()
    row = db.select().from(evobgpSettings).where(eq(evobgpSettings.id, 1)).limit(1).all()[0]
  }
  return row!
}

function normalizeBaseUrl(raw: string): string {
  const trimmed = raw.trim().replace(/\/$/, "")
  if (!trimmed) return ""
  try {
    const u = new URL(trimmed.startsWith("http") ? trimmed : `https://${trimmed}`)
    return `${u.protocol}//${u.host}`
  } catch {
    return trimmed
  }
}

/** Сырой API-ключ без префикса Bearer (иначе EvoBGP получит `Bearer Bearer …`). */
function normalizeApiKey(raw: string): string {
  const trimmed = raw.trim()
  return trimmed.replace(/^Bearer\s+/i, "").trim()
}

interface EvoCatalogRaw {
  modules: { items: Array<{ id: string; name: string; type: string }> }
  domains: {
    items: Array<{
      module_id: string
      entry: { id: string; fqdn: string; community_id?: string | null }
    }>
  }
  asns: {
    items: Array<{
      module_id: string
      entry: {
        id: string
        asn: number
        community_id?: string | null
        asn_name?: string | null
        prefix_count?: number | null
        asn_resolved_at?: string | null
      }
    }>
  }
  ip_ranges: {
    items: Array<{
      module_id: string
      entry: { id: string; prefix: string; community_id: string }
    }>
  }
  communities: {
    items: Array<{ id: string; community?: string; title?: string }>
  }
}

/**
 * По UUID community в каталоге EvoBGP: сумма «маршрутов» —
 * 1 на домен, 1 на CIDR в ip_ranges, prefix_count на ASN.
 */
function aggregateCommunityRouteCounts(catalog: EvoCatalogRaw): Map<string, number> {
  const routesPerComm = new Map<string, number>()
  function bump(commId: string, routeAdd: number) {
    routesPerComm.set(commId, (routesPerComm.get(commId) ?? 0) + routeAdd)
  }

  for (const rowItem of catalog.domains?.items ?? []) {
    const cid = rowItem.entry.community_id
    if (cid) bump(cid, 1)
  }
  for (const rowItem of catalog.ip_ranges?.items ?? []) {
    const cid = rowItem.entry.community_id
    if (cid) bump(cid, 1)
  }
  for (const rowItem of catalog.asns?.items ?? []) {
    const cid = rowItem.entry.community_id
    if (!cid) continue
    const pc = rowItem.entry.prefix_count
    const n = typeof pc === "number" && Number.isFinite(pc) && pc >= 0 ? Math.trunc(pc) : 0
    bump(cid, n)
  }

  return routesPerComm
}

/**
 * Уникальные серверы из локальной БД, у которых в BGP-фильтре указан community (строка AS:NNN).
 * Ключ — значение `filter_rules.community` после trim (как в EvoBGP `community`).
 */
function distinctServersByFilterCommunityValue(): Map<string, number> {
  const rows = db
    .select({ serverId: filterRules.serverId, community: filterRules.community })
    .from(filterRules)
    .all()
  const byVal = new Map<string, Set<number>>()
  for (const r of rows) {
    const v = (r.community ?? "").trim()
    if (!v) continue
    let set = byVal.get(v)
    if (!set) {
      set = new Set()
      byVal.set(v, set)
    }
    set.add(r.serverId)
  }
  const out = new Map<string, number>()
  for (const [v, set] of byVal) out.set(v, set.size)
  return out
}

function problemMessage(text: string, status: number): string {
  try {
    const j = JSON.parse(text) as { detail?: string; title?: string }
    return j.detail ?? j.title ?? `HTTP ${status}`
  } catch {
    return text || `HTTP ${status}`
  }
}

async function fetchEvoJson<T>(root: string, path: string, token: string): Promise<T> {
  const url = `${root}${path.startsWith("/") ? "" : "/"}${path}`
  const ac = new AbortController()
  const t = setTimeout(() => ac.abort(), 90_000)
  try {
    const res = await fetch(url, {
      method: "GET",
      headers: {
        Authorization: `Bearer ${token}`,
        Accept: "application/json",
      },
      signal: ac.signal,
    })
    const text = await res.text()
    if (!res.ok) {
      throw new Error(problemMessage(text, res.status))
    }
    return JSON.parse(text) as T
  } finally {
    clearTimeout(t)
  }
}

function credentialsFromDb(): { root: string; apiKey: string } | null {
  const row = ensureEvobgpRow()
  const root = normalizeBaseUrl(row.baseUrl)
  const apiKey = normalizeApiKey(row.apiKey)
  if (!root || !apiKey) return null
  return { root, apiKey }
}

const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
  app.get("/evobgp/settings", async (_req, reply) => {
    const row = ensureEvobgpRow()
    return reply.send({
      baseUrl: row.baseUrl ?? "",
      enabled: Boolean(row.enabled),
      secretConfigured: Boolean(normalizeApiKey(row.apiKey ?? "")),
    })
  })

  app.put("/evobgp/settings", async (req, reply) => {
    const parsed = PutEvobgpSchema.safeParse(req.body)
    if (!parsed.success) {
      return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
    }
    const cur = ensureEvobgpRow()
    let nextBase = cur.baseUrl
    let nextEnabled = cur.enabled
    let nextKey = cur.apiKey

    if (parsed.data.baseUrl !== undefined) {
      nextBase = normalizeBaseUrl(parsed.data.baseUrl)
    }
    if (parsed.data.enabled !== undefined) nextEnabled = parsed.data.enabled
    if (parsed.data.apiKey !== undefined) {
      nextKey =
        parsed.data.apiKey === null || parsed.data.apiKey === ""
          ? ""
          : normalizeApiKey(parsed.data.apiKey)
    }

    db.update(evobgpSettings)
      .set({
        baseUrl: nextBase,
        enabled: nextEnabled,
        apiKey: nextKey,
        updatedAt: sql`(datetime('now'))`,
      })
      .where(eq(evobgpSettings.id, 1))
      .run()

    const row = ensureEvobgpRow()
    return reply.send({
      baseUrl: row.baseUrl ?? "",
      enabled: Boolean(row.enabled),
      secretConfigured: Boolean(normalizeApiKey(row.apiKey ?? "")),
    })
  })

  /**
   * Проверка Bearer: тело опционально — можно передать черновики URL/ключа с формы.
   * Для каждого поля: если в JSON ключ отсутствует — используется значение из БД;
   * если `apiKey` передана пустой строкой — ключ берётся из БД (проверка только нового URL).
   */
  app.post("/evobgp/test", async (req, reply) => {
    const parsed = TestEvobgpSchema.safeParse(req.body ?? {})
    if (!parsed.success) {
      return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
    }
    const row = ensureEvobgpRow()
    const d = parsed.data
    const urlRaw = d.baseUrl !== undefined ? d.baseUrl : row.baseUrl
    const keyRaw =
      d.apiKey !== undefined && d.apiKey.trim() !== "" ? d.apiKey : row.apiKey
    const root = normalizeBaseUrl(urlRaw.trim())
    const token = normalizeApiKey(keyRaw)
    if (!root || !token) {
      return reply.status(400).send({
        error: "Нужны базовый URL и API-ключ (в форме или уже сохранённые в БД)",
      })
    }
    try {
      await fetchEvoJson<{ items: unknown[] }>(root, "/v1/modules?limit=1", token)
      return reply.send({ ok: true })
    } catch (e) {
      const msg = e instanceof Error ? e.message : "Ошибка EvoBGP"
      return reply.status(502).send({ error: msg })
    }
  })

  /**
   * Прокси к GET /v1/router-lists/catalog — учётные данные только из БД.
   */
  app.post("/evobgp/catalog", async (_req, reply) => {
    const row = ensureEvobgpRow()
    if (!row.enabled) {
      return reply.status(400).send({ error: "Интеграция EvoBGP выключена в настройках" })
    }
    const cred = credentialsFromDb()
    if (!cred) {
      return reply.status(400).send({ error: "Не заданы базовый URL или API-ключ в БД" })
    }
    try {
      const catalog = await fetchEvoJson<EvoCatalogRaw>(cred.root, "/v1/router-lists/catalog", cred.apiKey)
      const modules = catalog.modules?.items ?? []
      const modMap = new Map(modules.map((m) => [m.id, m]))
      const commItems = catalog.communities?.items ?? []
      const commLabel = new Map<string, string>()
      for (const c of commItems) {
        const label = (c.community && c.title)
          ? `${c.title} (${c.community})`
          : (c.title ?? c.community ?? c.id)
        commLabel.set(c.id, label)
      }

      const fmtTime = new Date().toLocaleString("ru-RU")
      const fetchedAt = new Date().toISOString()

      const domains = (catalog.domains?.items ?? []).map((rowItem) => {
        const mod = modMap.get(rowItem.module_id)
        const cid = rowItem.entry.community_id
        return {
          id: `${rowItem.module_id}:${rowItem.entry.id}`,
          domain: rowItem.entry.fqdn,
          resolvedIp: "—",
          asn: "—",
          purpose: mod?.name ?? "—",
          filter: cid ? (commLabel.get(cid) ?? cid) : "—",
          updated: fmtTime,
          enabled: true,
        }
      })

      const ipRanges = (catalog.ip_ranges?.items ?? []).map((rowItem) => {
        const mod = modMap.get(rowItem.module_id)
        const cid = rowItem.entry.community_id
        return {
          id: `${rowItem.module_id}:${rowItem.entry.id}`,
          cidr: rowItem.entry.prefix,
          asn: "—",
          country: "—",
          purpose: mod?.name ?? "—",
          filter: commLabel.get(cid) ?? cid,
          updated: fmtTime,
          enabled: true,
        }
      })

      const asns = (catalog.asns?.items ?? []).map((rowItem) => {
        const cid = rowItem.entry.community_id
        const e = rowItem.entry
        const name = (e.asn_name ?? "").trim()
        const pc = e.prefix_count
        const prefixes =
          typeof pc === "number" && Number.isFinite(pc) && pc >= 0 ? Math.trunc(pc) : 0
        let updated = fmtTime
        const rawResolved = (e.asn_resolved_at ?? "").trim()
        if (rawResolved) {
          const d = new Date(rawResolved)
          if (!Number.isNaN(d.getTime())) updated = d.toLocaleString("ru-RU")
        }
        return {
          id: `${rowItem.module_id}:${e.id}`,
          asn: `AS${e.asn}`,
          org: name || "—",
          country: "—",
          prefixes,
          filter: cid ? (commLabel.get(cid) ?? cid) : "—",
          updated,
          enabled: true,
        }
      })

      const routeCountsByCommUuid = aggregateCommunityRouteCounts(catalog)
      const serversByCommunityStr = distinctServersByFilterCommunityValue()

      const communities = commItems.map((c) => {
        const valueStr = (c.community ?? "").trim()
        return {
          id: c.id,
          value: c.community ?? c.id,
          name: c.title ?? c.community ?? c.id,
          description: "",
          type: "custom" as const,
          filterIds: [] as string[],
          serverCount: valueStr ? (serversByCommunityStr.get(valueStr) ?? 0) : 0,
          prefixCount: routeCountsByCommUuid.get(c.id) ?? 0,
          action: "permit" as const,
          enabled: true,
        }
      })

      return reply.send({
        fetchedAt,
        modules: modules.map((m) => ({ id: m.id, name: m.name, type: m.type })),
        domains,
        ipRanges,
        asns,
        communities,
      })
    } catch (e) {
      const msg = e instanceof Error ? e.message : "Ошибка EvoBGP"
      return reply.status(502).send({ error: msg })
    }
  })
}

export default evobgpRoutes
