import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { asc, desc, eq } from "drizzle-orm"
import { db } from "../db/index.js"
import { servers, uptimeSpeedProbes, uptimeSpeedTestRuns } from "../db/schema.js"
import { MikrotikClient } from "../services/mikrotik.js"
import { scheduleAlertEngineAfterDataCollectors } from "../services/alert-collector-hooks.js"
import { refreshScheduler, getSchedulerStatus } from "../services/scheduler.js"
import {
  collectPingForProbeIds,
  collectUptimeOnce,
  readProbeRows,
  readProbeSamplesSince,
  readResourceSamplesSince,
  readUptimeSettings,
  replaceProbes,
  updateProbeShowOnDashboard,
  updateUptimeSettings,
} from "../services/uptime-collector.js"
import { recordSpeedTestFailure, runBandwidthSpeedTest } from "../services/uptime-speed-service.js"

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 toSeries(values: number[], target = 40): number[] {
  if (values.length === 0) return Array(target).fill(0)
  if (values.length === target) return values
  if (values.length > target) return values.slice(values.length - target)
  return [...Array(target - values.length).fill(values[0] ?? 0), ...values]
}

/** Сэмпл ресурсов из SQLite (readResourceSamplesSince). */
type ResourceSampleRow = ReturnType<typeof readResourceSamplesSince>[number]

/**
 * Последний сэмпл по времени может быть offline после сбоя опроса — тогда UI «пустой».
 * Берём последний успешный (online + RAM), только если он не слишком старый относительно последней попытки,
 * иначе снова показываем устаревший uptime/метрики как раньше.
 */
function pickResourceDisplayRow(rows: ResourceSampleRow[], maxGapMs: number): {
  row: ResourceSampleRow | undefined
  hasData: boolean
} {
  if (rows.length === 0) return { row: undefined, hasData: false }
  const last = rows[rows.length - 1]!
  const lastTs = Date.parse(last.sampledAt)
  const validOnline = (r: ResourceSampleRow) => r.status === "online" && r.totalMemory > 0

  if (validOnline(last)) {
    return { row: last, hasData: true }
  }

  const fallback = [...rows].reverse().find(validOnline)
  if (!fallback) return { row: undefined, hasData: false }

  const fbTs = Date.parse(fallback.sampledAt)
  if (lastTs - fbTs <= maxGapMs) return { row: fallback, hasData: true }
  return { row: undefined, hasData: false }
}

const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
  app.get("/uptime/settings", async (_req, reply) => {
    const s = readUptimeSettings()
    return reply.send({
      enabled: s.enabled,
      resourcesEnabled: s.resourcesEnabled ?? s.enabled,
      pingEnabled: s.pingEnabled ?? s.enabled,
      speedEnabled: s.speedEnabled ?? s.enabled,
      /** Интервал сбора метрик ресурсов (сек). */
      intervalSec: s.intervalSec,
      /** Интервал тика ping и значение по умолчанию для проб без своего interval. */
      probeIntervalSec: s.probeIntervalSec,
      speedIntervalSec: s.speedIntervalSec ?? 60,
      retentionDays: s.retentionDays,
      lastCollectedAt: s.lastCollectedAt ?? null,
      lastDurationMs: s.lastDurationMs ?? null,
      lastError: s.lastError || null,
      scheduler: getSchedulerStatus(),
    })
  })

  app.put("/uptime/settings", async (req, reply) => {
    const body = req.body as {
      enabled?: boolean
      resourcesEnabled?: boolean
      pingEnabled?: boolean
      speedEnabled?: boolean
      intervalSec?: number | string
      probeIntervalSec?: number | string
      speedIntervalSec?: number | string
      retentionDays?: number | string
    }
    const updated = updateUptimeSettings({
      enabled: body.enabled,
      resourcesEnabled: body.resourcesEnabled,
      pingEnabled: body.pingEnabled,
      speedEnabled: body.speedEnabled,
      intervalSec: body.intervalSec == null ? undefined : Math.max(5, Number.parseInt(String(body.intervalSec), 10) || 300),
      probeIntervalSec: body.probeIntervalSec == null ? undefined : Math.max(5, Number.parseInt(String(body.probeIntervalSec), 10) || 15),
      speedIntervalSec: body.speedIntervalSec == null ? undefined : Math.max(10, Number.parseInt(String(body.speedIntervalSec), 10) || 60),
      retentionDays: body.retentionDays == null ? undefined : Math.max(1, Number.parseInt(String(body.retentionDays), 10) || 14),
    })
    refreshScheduler()
    return reply.send({
      ok: true,
      settings: {
        enabled: updated.enabled,
        resourcesEnabled: updated.resourcesEnabled ?? updated.enabled,
        pingEnabled: updated.pingEnabled ?? updated.enabled,
        speedEnabled: updated.speedEnabled ?? updated.enabled,
        intervalSec: updated.intervalSec,
        probeIntervalSec: updated.probeIntervalSec,
        speedIntervalSec: updated.speedIntervalSec ?? 60,
        retentionDays: updated.retentionDays,
        lastCollectedAt: updated.lastCollectedAt ?? null,
        lastDurationMs: updated.lastDurationMs ?? null,
        lastError: updated.lastError || null,
        scheduler: getSchedulerStatus(),
      },
    })
  })

  app.post("/uptime/collect-now", async (_req, reply) => {
    await collectUptimeOnce()
    scheduleAlertEngineAfterDataCollectors()
    const s = readUptimeSettings()
    return reply.send({
      ok: true,
      lastCollectedAt: s.lastCollectedAt ?? null,
      lastDurationMs: s.lastDurationMs ?? null,
      lastError: s.lastError || null,
    })
  })

  /** Ручной ping по списку id проб (группа «сервер + назначение» на UI); без ожидания interval_sec. */
  app.post("/uptime/probes/collect-group", async (req, reply) => {
    const body = req.body as { probeIds?: unknown }
    const raw = body.probeIds
    const probeIds = Array.isArray(raw) ? raw.filter((x): x is string => typeof x === "string") : []
    try {
      const { polled } = await collectPingForProbeIds(probeIds)
      if (polled > 0) scheduleAlertEngineAfterDataCollectors()
      return reply.send({ ok: true, polled })
    } catch (e) {
      const status = (e as Error & { statusCode?: number }).statusCode
      if (status === 409) {
        return reply.status(409).send({ error: e instanceof Error ? e.message : "busy" })
      }
      throw e
    }
  })

  app.get("/uptime/probes", async (_req, reply) => {
    const rows = readProbeRows()
    return reply.send({
      probes: rows.map((p) => ({
        id: p.id,
        srcServerId: String(p.srcServerId),
        srcInterface: p.srcInterface || "",
        name: p.name,
        target: p.target,
        probeFilter: p.probeFilter || "—",
        enabled: p.enabled,
        intervalSec: p.intervalSec ?? 0,
        showOnDashboard: p.showOnDashboard,
        sortOrder: p.sortOrder,
        createdAt: p.createdAt,
        updatedAt: p.updatedAt,
      })),
    })
  })

  app.get("/uptime/speed-probes", async (_req, reply) => {
    const rows = db.select().from(uptimeSpeedProbes).orderBy(asc(uptimeSpeedProbes.sortOrder)).all()
    return reply.send({
      probes: rows.map((r) => ({
        id: r.id,
        srcServerId: String(r.srcServerId),
        dstServerId: String(r.dstServerId),
        srcInterface: r.srcInterface || "",
        dstInterface: r.dstInterface || "",
        protocol: r.protocol === "udp" ? "udp" : "tcp",
        direction: r.direction === "transmit" || r.direction === "receive" ? r.direction : "both",
        durationSec: String(Math.max(3, r.durationSec || 10)),
        enabled: r.enabled !== false,
        lastRunAt: r.lastRunAt ?? null,
        lastTxAvgMbps: r.lastTxAvgMbps ?? null,
        lastRxAvgMbps: r.lastRxAvgMbps ?? null,
        lastStatus: r.lastStatus ?? null,
        lastError: r.lastError ?? null,
        lastPingRttMs: r.lastPingRttMs ?? null,
        lastPingLossPct: r.lastPingLossPct ?? null,
        lastPingAt: r.lastPingAt ?? null,
        lastPingError: r.lastPingError ?? null,
      })),
    })
  })

  app.get("/uptime/speed-test/runs", async (_req, reply) => {
    const rows = db.select().from(uptimeSpeedTestRuns).orderBy(desc(uptimeSpeedTestRuns.createdAt)).limit(200).all()
    return reply.send({
      runs: rows.map((r) => ({
        id: r.id,
        probeId: r.probeId ?? null,
        srcServerId: String(r.srcServerId),
        dstServerId: String(r.dstServerId),
        srcInterface: r.srcInterface || "",
        dstInterface: r.dstInterface || "",
        srcAddress: r.srcAddress ?? null,
        dstAddress: r.dstAddress ?? null,
        srcInterfaceAddress: r.srcInterfaceAddress ?? null,
        dstInterfaceAddress: r.dstInterfaceAddress ?? null,
        protocol: r.protocol === "udp" ? "udp" : "tcp",
        direction: r.direction === "transmit" || r.direction === "receive" ? r.direction : "both",
        durationSec: Math.max(3, r.durationSec || 10),
        txAvgMbps: r.txAvgMbps ?? 0,
        rxAvgMbps: r.rxAvgMbps ?? 0,
        afterBtPing: {
          rttMs: r.pingRttMs ?? null,
          lossPct: r.pingLossPct ?? null,
          error: r.pingError ?? null,
        },
        status: r.status === "error" ? "error" : "done",
        error: r.error ?? null,
        createdAt: r.createdAt,
      })),
    })
  })

  app.put("/uptime/speed-probes", async (req, reply) => {
    const body = req.body as {
      probes?: Array<{
        id?: string
        srcServerId?: string | number
        dstServerId?: string | number
        srcInterface?: string
        dstInterface?: string
        protocol?: "tcp" | "udp"
        direction?: "transmit" | "receive" | "both"
        durationSec?: string | number
        enabled?: boolean
      }>
    }
    const normalized = (body.probes ?? [])
      .map((p, i) => ({
        id: p.id || `sp-${Date.now()}-${i}`,
        srcServerId: Number.parseInt(String(p.srcServerId ?? ""), 10),
        dstServerId: Number.parseInt(String(p.dstServerId ?? ""), 10),
        srcInterface: String(p.srcInterface ?? "").trim(),
        dstInterface: String(p.dstInterface ?? "").trim(),
        protocol: p.protocol === "udp" ? "udp" : "tcp",
        direction: p.direction === "transmit" || p.direction === "receive" ? p.direction : "both",
        durationSec: Math.max(3, Number.parseInt(String(p.durationSec ?? ""), 10) || 10),
        enabled: p.enabled !== false,
      }))
      .filter((p) => Number.isFinite(p.srcServerId) && Number.isFinite(p.dstServerId) && p.srcServerId !== p.dstServerId)

    const now = new Date().toISOString()
    const existing = db.select({ id: uptimeSpeedProbes.id }).from(uptimeSpeedProbes).all()
    const nextIds = new Set(normalized.map((p) => p.id))

    for (const row of existing) {
      if (nextIds.has(row.id)) continue
      db.delete(uptimeSpeedProbes).where(eq(uptimeSpeedProbes.id, row.id)).run()
    }

    for (let i = 0; i < normalized.length; i += 1) {
      const p = normalized[i]
      const patch = {
        srcServerId: p.srcServerId,
        dstServerId: p.dstServerId,
        srcInterface: p.srcInterface,
        dstInterface: p.dstInterface,
        protocol: p.protocol as "tcp" | "udp",
        direction: p.direction as "transmit" | "receive" | "both",
        durationSec: p.durationSec,
        enabled: p.enabled,
        sortOrder: i,
        updatedAt: now,
      }
      const updated = db.update(uptimeSpeedProbes).set(patch).where(eq(uptimeSpeedProbes.id, p.id)).run()
      if ((updated.changes ?? 0) > 0) continue
      db.insert(uptimeSpeedProbes).values({
        id: p.id,
        ...patch,
        createdAt: now,
      }).run()
    }
    refreshScheduler()
    return reply.send({ ok: true })
  })

  app.put("/uptime/probes", async (req, reply) => {
    const body = req.body as {
      probes?: Array<{
        id?: string
        srcServerId?: string | number
        srcInterface?: string
        name?: string
        target?: string
        filter?: string
        enabled?: boolean
        showOnDashboard?: boolean
        intervalSec?: number | string
      }>
    }
    const normalized = (body.probes ?? [])
      .map((p, i) => ({
        id: p.id || `p-${Date.now()}-${i}`,
        srcServerId: Number.parseInt(String(p.srcServerId ?? ""), 10),
        srcInterface: String(p.srcInterface ?? "").trim(),
        name: String(p.name ?? "").trim(),
        target: String(p.target ?? "").trim(),
        probeFilter: String(p.filter ?? "—"),
        enabled: p.enabled !== false,
        showOnDashboard: p.showOnDashboard === true,
        intervalSec: p.intervalSec == null || p.intervalSec === ""
          ? undefined
          : Number.parseInt(String(p.intervalSec), 10),
      }))
      .filter((p) => Number.isFinite(p.srcServerId) && p.name && p.target)
    replaceProbes(normalized)
    refreshScheduler()
    return reply.send({ ok: true })
  })

  app.patch("/uptime/probes/:id", async (req, reply) => {
    const params = req.params as { id?: string }
    const probeId = String(params.id ?? "").trim()
    if (!probeId) return reply.status(400).send({ error: "Invalid probe id" })
    const body = req.body as { showOnDashboard?: boolean }
    if (typeof body.showOnDashboard !== "boolean") {
      return reply.status(400).send({ error: "showOnDashboard boolean required" })
    }
    const ok = updateProbeShowOnDashboard(probeId, body.showOnDashboard)
    if (!ok) return reply.status(404).send({ error: "Probe not found" })
    return reply.send({ ok: true })
  })

  app.get("/uptime/sources/:id/interfaces", async (req, reply) => {
    const params = req.params as { id?: string | number }
    const serverId = Number.parseInt(String(params.id ?? ""), 10)
    if (!Number.isFinite(serverId)) return reply.status(400).send({ error: "Invalid server id" })
    const srv = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
    if (!srv) return reply.status(404).send({ error: "Server not found" })
    try {
      const rows = await MikrotikClient.fromServer(srv).getInterfaces()
      const interfaces = rows
        .map((r) => ({
          name: String(r.name ?? "").trim(),
          running: String(r.running ?? "false").toLowerCase() === "true",
          disabled: String(r.disabled ?? "false").toLowerCase() === "true",
        }))
        .filter((r) => r.name.length > 0)
        .filter((r) => r.running && !r.disabled)
        .sort((a, b) => a.name.localeCompare(b.name))
      return reply.send({ interfaces })
    } catch (e) {
      const message = e instanceof Error ? e.message : "Failed to fetch interfaces"
      return reply.status(502).send({ error: message })
    }
  })

  app.post("/uptime/speed-test", async (req, reply) => {
    const body = req.body as {
      runId?: string
      probeId?: string
      srcServerId?: string | number
      dstServerId?: string | number
      srcInterface?: string
      dstInterface?: string
      protocol?: "tcp" | "udp"
      direction?: "transmit" | "receive" | "both"
      durationSec?: string | number
    }
    const srcId = Number.parseInt(String(body.srcServerId ?? ""), 10)
    const dstId = Number.parseInt(String(body.dstServerId ?? ""), 10)
    if (!Number.isFinite(srcId) || !Number.isFinite(dstId)) {
      return reply.status(400).send({ error: "Invalid src/dst server id" })
    }
    if (srcId === dstId) return reply.status(400).send({ error: "Source and destination must be different" })
    const src = db.select().from(servers).where(eq(servers.id, srcId)).limit(1).all()[0]
    const dst = db.select().from(servers).where(eq(servers.id, dstId)).limit(1).all()[0]
    if (!src || !dst) return reply.status(404).send({ error: "Server not found" })
    try {
      const result = await runBandwidthSpeedTest({
        runId: String(body.runId ?? "").trim() || undefined,
        probeId: String(body.probeId ?? "").trim() || undefined,
        srcServerId: srcId,
        dstServerId: dstId,
        srcInterface: String(body.srcInterface ?? "").trim() || undefined,
        dstInterface: String(body.dstInterface ?? "").trim() || undefined,
        protocol: body.protocol === "udp" ? "udp" : "tcp",
        direction: body.direction === "transmit" || body.direction === "receive" ? body.direction : "both",
        durationSec: Math.max(3, Number.parseInt(String(body.durationSec ?? ""), 10) || 10),
      })
      return reply.send({ ok: true, result })
    } catch (e) {
      const raw = e instanceof Error ? e.message : "Bandwidth test failed"
      recordSpeedTestFailure({
        probeId: String(body.probeId ?? "").trim() || undefined,
        runId: String(body.runId ?? "").trim() || undefined,
        srcServerId: srcId,
        dstServerId: dstId,
        srcInterface: String(body.srcInterface ?? "").trim(),
        dstInterface: String(body.dstInterface ?? "").trim(),
        protocol: body.protocol === "udp" ? "udp" : "tcp",
        direction: body.direction === "transmit" || body.direction === "receive" ? body.direction : "both",
        durationSec: Math.max(3, Number.parseInt(String(body.durationSec ?? ""), 10) || 10),
        message: raw,
      })
      const message = /timed out|socket hang up|ECONNRESET/i.test(raw)
        ? "BTTest timeout/socket hang up: проверь доступность destination, /tool bandwidth-server, firewall и корректность интерфейсов"
        : raw
      if (/активного IP|Server not found/i.test(raw)) {
        return reply.status(400).send({ error: message })
      }
      return reply.status(502).send({ error: message })
    }
  })

  app.get("/uptime/speed-test/endpoints/:id/interfaces", async (req, reply) => {
    const params = req.params as { id?: string | number }
    const serverId = Number.parseInt(String(params.id ?? ""), 10)
    if (!Number.isFinite(serverId)) return reply.status(400).send({ error: "Invalid server id" })
    const srv = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
    if (!srv) return reply.status(404).send({ error: "Server not found" })
    try {
      const client = MikrotikClient.fromServer(srv)
      const [ifaces, ips] = await Promise.all([
        client.getInterfaces(),
        client.getIpAddresses(),
      ])
      const interfaces = ifaces
        .map((i) => {
          const name = String(i.name ?? "").trim()
          const addresses = ips
            .filter((a) => String(a.interface ?? "") === name)
            .filter((a) => String(a.disabled ?? "false").toLowerCase() !== "true")
            .filter((a) => String(a.invalid ?? "false").toLowerCase() !== "true")
            .sort((a, b) => {
              const ad = String(a.dynamic ?? "false").toLowerCase() === "true" ? 1 : 0
              const bd = String(b.dynamic ?? "false").toLowerCase() === "true" ? 1 : 0
              return ad - bd
            })
            .map((a) => String(a.address ?? "").split("/")[0])
            .filter((x) => x.length > 0)
          return {
            name,
            running: String(i.running ?? "false").toLowerCase() === "true",
            disabled: String(i.disabled ?? "false").toLowerCase() === "true",
            addresses,
          }
        })
        .filter((i) => i.name.length > 0)
        .filter((i) => i.running && !i.disabled)
      return reply.send({ interfaces })
    } catch (e) {
      const message = e instanceof Error ? e.message : "Failed to fetch speed-test interfaces"
      return reply.status(502).send({ error: message })
    }
  })

  app.get("/uptime/overview", async (req, reply) => {
    const q = req.query as { range?: string }
    const minutes = rangeToMinutes(q.range)
    const sinceIso = new Date(Date.now() - minutes * 60_000).toISOString()

    const allServers = db.select().from(servers).all()
    const probeRows = readProbeRows()
    const probeSamples = readProbeSamplesSince(sinceIso)
    const uptimeCfg = readUptimeSettings()
    const intervalSec = Math.max(15, uptimeCfg.intervalSec ?? 300)
    /** Допустимый разрыв между последней попыткой и последним «хорошим» сэмплом (не показывать данные часовой давности). */
    const resourceFallbackMaxGapMs = Math.min(2 * 3600 * 1000, Math.max(10 * 60 * 1000, intervalSec * 6 * 1000))

    const probes = probeRows.map((p) => {
      const rows = probeSamples.filter((r) => r.probeId === p.id)
      const last = rows[rows.length - 1]
      const series = toSeries(rows.map((r) => r.rttMs ?? 0))
      return {
        id: p.id,
        srcServerId: String(p.srcServerId),
        srcInterface: p.srcInterface || "",
        name: p.name,
        target: p.target,
        filter: p.probeFilter || "—",
        rtt: last?.rttMs ?? null,
        loss: last?.lossPct ?? 100,
        status: (last?.status ?? "down") as "up" | "warn" | "down",
        series,
        enabled: p.enabled,
        showOnDashboard: p.showOnDashboard,
      }
    })

    const resources = allServers.map((s) => {
      const rows = readResourceSamplesSince(sinceIso, s.id)
      const { row: pick, hasData } = pickResourceDisplayRow(rows, resourceFallbackMaxGapMs)
      const cpuHistory = toSeries(rows.map((r) => r.cpuLoad), 40)
      return {
        serverId: String(s.id),
        hasData,
        cpu: pick?.cpuLoad ?? 0,
        cpuHistory,
        ramUsed: Math.max(0, ((pick?.totalMemory ?? 0) - (pick?.freeMemory ?? 0)) / (1024 * 1024)),
        ramTotal: Math.max(0, (pick?.totalMemory ?? 0) / (1024 * 1024)),
        hddUsed: Math.max(0, ((pick?.totalHddSpace ?? 0) - (pick?.freeHddSpace ?? 0)) / (1024 * 1024)),
        hddTotal: Math.max(0, (pick?.totalHddSpace ?? 0) / (1024 * 1024)),
        uptimeSeconds: pick?.uptimeSeconds ?? 0,
        boardName: pick?.boardName || "RouterBOARD",
        temp: undefined as number | undefined,
      }
    })

    return reply.send({ probes, resources })
  })
}

export default uptimeRoutes

