import { env } from "../config.js"
import { desc, eq } from "drizzle-orm"
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { db } from "../db/index.js"
import { serverSnapshots, servers } from "../db/schema.js"
import {
  collectTrafficOnce,
  getTrafficCollectorState,
  getTrafficSettings,
  readServerSamplesInRange,
  updateTrafficSettings,
} from "../services/traffic-collector.js"
import { scheduleAlertEngineAfterDataCollectors } from "../services/alert-collector-hooks.js"
import { refreshScheduler } from "../services/scheduler.js"
import { appendEvent } from "../modules/events/service/events-service.js"
import { MikrotikClient } from "../services/mikrotik.js"
import { getEnabledServerById } from "../services/wireguard-live.js"
import {
  bpsToMbps,
  buildTrafficFromSamples,
  isLoopbackName,
  parseMonitorTraffic,
  rateBpsFromDelta,
} from "../services/traffic-rate.js"
import {
  buildBoundInterfaceTraffic,
  buildUserTrafficList,
} from "../services/traffic-users.js"

type SnapshotRow = typeof serverSnapshots.$inferSelect

interface TrafficServerDto {
  id: string
  name: string
  site: string
  country: string
  status: "online" | "offline" | "degraded"
  rxNow: number
  txNow: number
  rxPeak: number
  txPeak: number
  rxTotal: number
  txTotal: number
  sessions: number
  rxSeries: number[]
  txSeries: number[]
}

interface TrafficInterfaceDto {
  name: string
  running: boolean
  disabled: boolean
  rxNow: number
  txNow: number
}

const LIVE_TICK_MS = 1500
const LIVE_ROS_TIMEOUT_MS = 4000

function latestSnapshot(serverId: number): SnapshotRow | undefined {
  return db
    .select()
    .from(serverSnapshots)
    .where(eq(serverSnapshots.serverId, serverId))
    .orderBy(desc(serverSnapshots.polledAt))
    .limit(1)
    .all()[0]
}

function rangeToMinutes(range: string | undefined): number {
  switch ((range ?? "1h").toLowerCase()) {
    case "5m": return 5
    case "15m": return 15
    case "1h": return 60
    case "4h": return 240
    case "24h": return 1440
    default: return 60
  }
}

function buildServerTraffic(
  s: typeof servers.$inferSelect,
  status: TrafficServerDto["status"],
  rows: Array<{
    interfaceName: string
    sampledAt: string
    rxBps: number
    txBps: number
    rxBytes: number
    txBytes: number
    running: boolean
    disabled: boolean
  }>,
  rangeStartMs: number,
  rangeEndMs: number,
  onlyInterface?: string,
): TrafficServerDto {
  const built = buildTrafficFromSamples(rows, rangeStartMs, rangeEndMs, onlyInterface)
  return {
    id: String(s.id),
    name: s.name || s.host,
    site: s.site || "—",
    country: s.country || "UN",
    status,
    rxNow: built.rxNow,
    txNow: built.txNow,
    rxPeak: built.rxPeak,
    txPeak: built.txPeak,
    rxTotal: built.rxTotalGiB,
    txTotal: built.txTotalGiB,
    sessions: built.sessions,
    rxSeries: built.rxSeries,
    txSeries: built.txSeries,
  }
}

function snapshotStatus(serverId: number): TrafficServerDto["status"] {
  const snap = latestSnapshot(serverId)
  return snap?.status === "offline" ? "offline" : (snap?.status === "online" ? "online" : "degraded")
}

function ifaceNowMbps(
  prev: { rxBytes: number; txBytes: number; sampledAt: string } | undefined,
  last: { rxBytes: number; txBytes: number; sampledAt: string; rxBps: number; txBps: number },
): { rxNow: number; txNow: number } {
  if (!prev) {
    return { rxNow: bpsToMbps(last.rxBps), txNow: bpsToMbps(last.txBps) }
  }
  const t0 = Date.parse(prev.sampledAt)
  const t1 = Date.parse(last.sampledAt)
  const rxBps = rateBpsFromDelta(prev.rxBytes, last.rxBytes, t0, t1)
  const txBps = rateBpsFromDelta(prev.txBytes, last.txBytes, t0, t1)
  return {
    rxNow: bpsToMbps(rxBps ?? last.rxBps),
    txNow: bpsToMbps(txBps ?? last.txBps),
  }
}

function writeSse(raw: NodeJS.WritableStream, event: string, data: unknown) {
  raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
}

function sleep(ms: number, signal: AbortSignal): Promise<void> {
  return new Promise((resolve, reject) => {
    if (signal.aborted) {
      reject(new Error("aborted"))
      return
    }
    const timer = setTimeout(() => {
      signal.removeEventListener("abort", onAbort)
      resolve()
    }, ms)
    const onAbort = () => {
      clearTimeout(timer)
      reject(new Error("aborted"))
    }
    signal.addEventListener("abort", onAbort, { once: true })
  })
}

function flattenMonitor(raw: unknown): unknown[] {
  if (Array.isArray(raw)) return raw
  if (raw != null) return [raw]
  return []
}

async function listRunningIfaceNames(client: MikrotikClient): Promise<string[]> {
  const ifaces = await client.get<Array<{ name?: string; running?: string; disabled?: string }>>(
    "/interface",
    LIVE_ROS_TIMEOUT_MS,
  )
  return ifaces
    .filter((i) => (i.running ?? "false") === "true"
      && (i.disabled ?? "false") !== "true"
      && !isLoopbackName(i.name ?? ""))
    .map((i) => i.name ?? "")
    .filter(Boolean)
}

async function monitorTrafficOnce(
  client: MikrotikClient,
  onlyInterface: string | undefined,
  cache: { names: string[]; joinedFailed: boolean },
  signal: AbortSignal,
): Promise<unknown> {
  if (onlyInterface) {
    return client.post(
      "/interface/monitor-traffic",
      { interface: onlyInterface, once: "" },
      LIVE_ROS_TIMEOUT_MS,
      signal,
    )
  }
  if (cache.names.length === 0) {
    cache.names = await listRunningIfaceNames(client)
  }
  if (cache.names.length === 0) return []
  if (!cache.joinedFailed) {
    try {
      return await client.post(
        "/interface/monitor-traffic",
        { interface: cache.names.join(","), once: "" },
        LIVE_ROS_TIMEOUT_MS,
        signal,
      )
    } catch {
      cache.joinedFailed = true
    }
  }
  const chunks = await Promise.all(
    cache.names.map((name) =>
      client.post(
        "/interface/monitor-traffic",
        { interface: name, once: "" },
        LIVE_ROS_TIMEOUT_MS,
        signal,
      ).then(flattenMonitor).catch(() => [] as unknown[]),
    ),
  )
  return chunks.flat()
}

const trafficRoutes: FastifyPluginAsyncZod = async (app) => {
  app.get("/traffic/settings", async (_req, reply) => {
    const settings = getTrafficSettings()
    const state = getTrafficCollectorState()
    return reply.send({
      enabled: settings.enabled,
      intervalSec: settings.intervalSec,
      retentionDays: settings.retentionDays,
      lastCollectedAt: settings.lastCollectedAt ?? null,
      lastDurationMs: settings.lastDurationMs ?? null,
      lastError: settings.lastError || null,
      collectorRunning: state.running,
    })
  })

  app.put("/traffic/settings", async (req, reply) => {
    const body = req.body as {
      enabled?: boolean
      intervalSec?: number | string
      retentionDays?: number | string
    }
    const intervalSec = body.intervalSec == null ? undefined : Math.max(5, Number.parseInt(String(body.intervalSec), 10) || 30)
    const retentionDays = body.retentionDays == null ? undefined : Math.max(1, Number.parseInt(String(body.retentionDays), 10) || 14)
    const updated = updateTrafficSettings({
      enabled: body.enabled,
      intervalSec,
      retentionDays,
    })
    refreshScheduler()
    return reply.send({
      ok: true,
      settings: {
        enabled: updated.enabled,
        intervalSec: updated.intervalSec,
        retentionDays: updated.retentionDays,
        lastCollectedAt: updated.lastCollectedAt ?? null,
        lastDurationMs: updated.lastDurationMs ?? null,
        lastError: updated.lastError || null,
      },
    })
  })

  app.post("/traffic/collect-now", async (_req, reply) => {
    try {
      await collectTrafficOnce()
      scheduleAlertEngineAfterDataCollectors()
      const updated = getTrafficSettings()
      appendEvent({
        level: "info",
        eventType: "traffic.collect.manual.ok",
        sourceModule: "traffic",
        title: "Ручной сбор трафика завершен",
        message: `Длительность: ${updated.lastDurationMs ?? 0} мс`,
        payload: {
          lastCollectedAt: updated.lastCollectedAt ?? null,
        },
      })
      return reply.send({
        ok: true,
        lastCollectedAt: updated.lastCollectedAt ?? null,
        lastDurationMs: updated.lastDurationMs ?? null,
        lastError: updated.lastError || null,
      })
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error)
      appendEvent({
        level: "critical",
        eventType: "traffic.collect.manual.failed",
        sourceModule: "traffic",
        title: "Ошибка ручного сбора трафика",
        message: msg,
      })
      return reply.status(500).send({ error: msg })
    }
  })

  app.get("/traffic/servers", async (req, reply) => {
    const q = req.query as { range?: string }
    const minutes = rangeToMinutes(q.range)
    const rangeEndMs = Date.now()
    const rangeStartMs = rangeEndMs - minutes * 60_000
    const sinceIso = new Date(rangeStartMs).toISOString()
    const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
    const data = allServers.map((s): TrafficServerDto => {
      const rows = readServerSamplesInRange(s.id, sinceIso)
      return buildServerTraffic(s, snapshotStatus(s.id), rows, rangeStartMs, rangeEndMs)
    })

    return reply.send({ servers: data })
  })

  app.get("/traffic/servers/:id/live", async (req, reply) => {
    const p = req.params as { id?: string | number }
    const q = req.query as { iface?: string }
    const server = getEnabledServerById(p.id ?? "")
    if (!server || !server.enabled) return reply.status(404).send({ error: "Server not found" })

    const onlyInterface = q.iface && q.iface !== "__all__" ? q.iface : undefined
    const abort = new AbortController()
    const onClose = () => abort.abort()
    req.raw.on("close", onClose)

    reply.hijack()
    req.raw.setTimeout(0)
    reply.raw.setTimeout(0)
    const origin = typeof req.headers.origin === "string" ? req.headers.origin : ""
    const allowed = env.CORS_ORIGIN
    const sseHeaders: Record<string, string> = {
      "Content-Type": "text/event-stream; charset=utf-8",
      "Cache-Control": "no-cache, no-transform",
      Connection: "keep-alive",
      "X-Accel-Buffering": "no",
    }
    if (origin && (allowed === "*" || allowed === origin)) {
      sseHeaders["Access-Control-Allow-Origin"] = origin
      sseHeaders["Access-Control-Allow-Credentials"] = "true"
      sseHeaders["Access-Control-Allow-Headers"] = "Authorization, Accept"
      sseHeaders.Vary = "Origin"
    }
    reply.raw.writeHead(200, sseHeaders)
    reply.raw.write(":\n\n")

    const client = MikrotikClient.fromServer(server)
    const cache = { names: onlyInterface ? [onlyInterface] : [] as string[], joinedFailed: false }

    try {
      while (!abort.signal.aborted) {
        try {
          const raw = await monitorTrafficOnce(client, onlyInterface, cache, abort.signal)
          const sample = parseMonitorTraffic(raw, { onlyInterface })
          writeSse(reply.raw, "sample", sample)
        } catch (error) {
          if (abort.signal.aborted) break
          const msg = error instanceof Error ? error.message : String(error)
          writeSse(reply.raw, "error", { error: msg })
        }
        await sleep(LIVE_TICK_MS, abort.signal)
      }
    } catch {
      /* abort / disconnect */
    } finally {
      req.raw.off("close", onClose)
      try {
        reply.raw.end()
      } catch {
        /* already closed */
      }
    }
  })

  app.get("/traffic/servers/:id/interfaces", async (req, reply) => {
    const p = req.params as { id?: string | number }
    const serverId = Number.parseInt(String(p.id ?? ""), 10)
    if (!Number.isFinite(serverId)) return reply.status(400).send({ error: "id is required" })

    const allRows = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()
    const server = allRows[0]
    if (!server) return reply.status(404).send({ error: "Server not found" })

    const sinceIso = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()
    const rows = readServerSamplesInRange(serverId, sinceIso)
    const byIface = new Map<string, typeof rows>()
    for (const r of rows) {
      if (isLoopbackName(r.interfaceName)) continue
      const arr = byIface.get(r.interfaceName) ?? []
      arr.push(r)
      byIface.set(r.interfaceName, arr)
    }
    const interfaces: TrafficInterfaceDto[] = [...byIface.entries()].map(([name, arr]) => {
      const sorted = arr.sort((a, b) => a.sampledAt.localeCompare(b.sampledAt))
      const last = sorted[sorted.length - 1]
      const prev = sorted[sorted.length - 2]
      if (!last) {
        return { name, running: false, disabled: true, rxNow: 0, txNow: 0 }
      }
      const now = ifaceNowMbps(prev, last)
      return {
        name,
        running: Boolean(last.running),
        disabled: Boolean(last.disabled),
        rxNow: now.rxNow,
        txNow: now.txNow,
      }
    }).sort((a, b) => (b.rxNow + b.txNow) - (a.rxNow + a.txNow))

    return reply.send({ interfaces })
  })

  app.get("/traffic/servers/:id", async (req, reply) => {
    const p = req.params as { id?: string | number }
    const q = req.query as { range?: string; iface?: string }
    const serverId = Number.parseInt(String(p.id ?? ""), 10)
    if (!Number.isFinite(serverId)) return reply.status(400).send({ error: "id is required" })
    const server = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
    if (!server) return reply.status(404).send({ error: "Server not found" })

    const minutes = rangeToMinutes(q.range)
    const rangeEndMs = Date.now()
    const rangeStartMs = rangeEndMs - minutes * 60_000
    const sinceIso = new Date(rangeStartMs).toISOString()
    const rows = readServerSamplesInRange(server.id, sinceIso)
    const data = buildServerTraffic(
      server,
      snapshotStatus(server.id),
      rows,
      rangeStartMs,
      rangeEndMs,
      q.iface && q.iface !== "__all__" ? q.iface : undefined,
    )
    return reply.send({ server: data })
  })

  app.get("/traffic/users", async (req, reply) => {
    const q = req.query as { range?: string }
    const minutes = rangeToMinutes(q.range)
    const rangeEndMs = Date.now()
    const rangeStartMs = rangeEndMs - minutes * 60_000
    return reply.send({ users: buildUserTrafficList(rangeStartMs, rangeEndMs) })
  })

  app.get("/traffic/users/:id", async (req, reply) => {
    const p = req.params as { id?: string }
    const q = req.query as { range?: string }
    const id = String(p.id ?? "")
    if (!id) return reply.status(400).send({ error: "id is required" })
    const minutes = rangeToMinutes(q.range)
    const rangeEndMs = Date.now()
    const rangeStartMs = rangeEndMs - minutes * 60_000
    const users = buildUserTrafficList(rangeStartMs, rangeEndMs)
    const user = users.find((u) => u.id === id)
    if (!user) return reply.status(404).send({ error: "Пользователь не найден" })
    return reply.send({ user })
  })

  app.get("/traffic/bound-interfaces", async (req, reply) => {
    const q = req.query as { range?: string }
    const minutes = rangeToMinutes(q.range)
    const rangeEndMs = Date.now()
    const rangeStartMs = rangeEndMs - minutes * 60_000
    return reply.send({ interfaces: buildBoundInterfaceTraffic(rangeStartMs, rangeEndMs) })
  })
}

export default trafficRoutes
