import { asc, eq } from "drizzle-orm"
import { db } from "../db/index.js"
import { servers, uptimeSpeedProbes, uptimeSpeedTestRuns } from "../db/schema.js"
import type { SpeedRunSnapshot, SpeedScheduledRunSnapshot } from "../types/scheduler-run-snapshot.js"
import { SCHEDULER_RUN_SNAPSHOT_VERSION } from "../types/scheduler-run-snapshot.js"
import type { RosIpAddress } from "../types/server.js"
import { withBtestNodeLocks } from "./btest-node-locks.js"
import { MikrotikClient } from "./mikrotik.js"
import { parsePing } from "./uptime-collector.js"

function firstUsableIpOnInterface(rows: RosIpAddress[], iface: string): string {
  const name = iface.trim()
  const candidates = rows.filter((a) => {
    if (String(a.interface ?? "").trim() !== name) return false
    if (String(a.disabled ?? "false").toLowerCase() === "true") return false
    if (String(a.invalid ?? "false").toLowerCase() === "true") return false
    const addr = String(a.address ?? "").trim()
    return addr.includes("/")
  })
  candidates.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
  })
  const raw = candidates[0]?.address
  return raw ? String(raw).split("/")[0]?.trim() ?? "" : ""
}

function parseRateToMbps(raw: unknown): number {
  const txt = String(raw ?? "").trim().toLowerCase()
  if (!txt) return 0
  const m = txt.match(/^(\d+(?:\.\d+)?)\s*(g|m|k)?(?:bps|bit\/s|bits\/s)?$/)
  if (m) {
    const n = Number.parseFloat(m[1])
    const u = m[2] ?? ""
    if (!Number.isFinite(n)) return 0
    if (u === "g") return n * 1000
    if (u === "m") return n
    if (u === "k") return n / 1000
    return n / 1_000_000
  }
  const n = Number.parseFloat(txt.replace(/[^\d.]/g, ""))
  if (!Number.isFinite(n)) return 0
  return n > 10000 ? n / 1_000_000 : n
}

export interface SpeedTestRunParams {
  runId?: string
  probeId?: string
  srcServerId: number
  dstServerId: number
  srcInterface?: string
  dstInterface?: string
  protocol?: "tcp" | "udp"
  direction?: "transmit" | "receive" | "both"
  durationSec?: number
}

export interface SpeedTestRunResult {
  runId: string
  srcInterface: string | null
  dstInterface: string | null
  address: string
  srcAddress: string
  dstAddress: string
  srcInterfaceAddress: string | null
  dstInterfaceAddress: string | null
  protocol: "tcp" | "udp"
  direction: "transmit" | "receive" | "both"
  durationSec: number
  txAvgMbps: number
  rxAvgMbps: number
  txSeries: number[]
  rxSeries: number[]
  raw: Array<Record<string, string>>
  afterBtPing: {
    rttMs: number | null
    lossPct: number | null
    error: string | null
  }
}

function humanizeSpeedError(raw: string): string {
  return /timed out|socket hang up|ECONNRESET/i.test(raw)
    ? "BTTest timeout/socket hang up: проверь доступность destination, /tool bandwidth-server, firewall и корректность интерфейсов"
    : raw
}

/** Полный цикл bandwidth-test + after-ping + запись в БД. Вызывать внутри withBtestNodeLocks при необходимости — здесь lock уже снаружи. */
export async function executeBandwidthSpeedTestInner(params: SpeedTestRunParams): Promise<SpeedTestRunResult> {
  const srcId = params.srcServerId
  const dstId = params.dstServerId
  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) throw new Error("Server not found")

  const protocol = params.protocol === "udp" ? "udp" : "tcp"
  const direction = params.direction === "transmit" || params.direction === "receive" ? params.direction : "both"
  const durationSec = Math.max(3, Number.parseInt(String(params.durationSec ?? ""), 10) || 10)
  const srcInterface = String(params.srcInterface ?? "").trim()
  const dstInterface = String(params.dstInterface ?? "").trim()
  const srcClient = MikrotikClient.fromServer(src)
  const dstAddressRows = dstInterface
    ? await MikrotikClient.fromServer(dst).getIpAddresses().catch(() => [])
    : []
  const srcAddressRows = srcInterface
    ? await srcClient.getIpAddresses().catch(() => [])
    : []
  const dstAddress = dstInterface ? firstUsableIpOnInterface(dstAddressRows, dstInterface) : dst.host
  if (dstInterface && !dstAddress) {
    throw new Error(`На интерфейсе назначения '${dstInterface}' нет активного IP (проверьте /ip/address — не disabled).`)
  }
  const initialAddress = dstAddress || dst.host
  const srcAddress = srcInterface ? firstUsableIpOnInterface(srcAddressRows, srcInterface) : src.host
  const srcInterfaceAddress = srcInterface ? (srcAddress || null) : null
  const dstInterfaceAddress = dstInterface ? (dstAddress || null) : null

  const rows = await srcClient.bandwidthTest({
    address: initialAddress,
    user: dst.username,
    password: dst.password,
    protocol,
    direction,
    durationSec,
  })
  const txSeries = rows
    .map((r) => parseRateToMbps(r["tx-current"] ?? r["tx-10-second-average"] ?? r["tx-total-average"]))
    .filter((v) => Number.isFinite(v) && v >= 0)
  const rxSeries = rows
    .map((r) => parseRateToMbps(r["rx-current"] ?? r["rx-10-second-average"] ?? r["rx-total-average"]))
    .filter((v) => Number.isFinite(v) && v >= 0)
  const last = rows[rows.length - 1] ?? {}
  const txAvg = parseRateToMbps(last["tx-total-average"] ?? last["tx-10-second-average"])
  const rxAvg = parseRateToMbps(last["rx-total-average"] ?? last["rx-10-second-average"])
  const txFinal = txAvg || (txSeries.length ? txSeries.reduce((a, b) => a + b, 0) / txSeries.length : 0)
  const rxFinal = rxAvg || (rxSeries.length ? rxSeries.reduce((a, b) => a + b, 0) / rxSeries.length : 0)

  await new Promise<void>((resolve) => setTimeout(resolve, 3000))

  const pingNow = new Date().toISOString()
  let afterBtPing: SpeedTestRunResult["afterBtPing"] = { rttMs: null, lossPct: null, error: null }
  try {
    const pingRows = await srcClient.ping(initialAddress, 4, srcInterface || undefined, { interval: "1s" })
    const pr = parsePing(pingRows as Parameters<typeof parsePing>[0])
    afterBtPing = { rttMs: pr.avgRtt, lossPct: pr.loss, error: null }
  } catch (pe) {
    afterBtPing = {
      rttMs: null,
      lossPct: null,
      error: pe instanceof Error ? pe.message : String(pe),
    }
  }

  const probeId = String(params.probeId ?? "").trim()
  const runId = String(params.runId ?? "").trim() || `sr-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
  if (probeId) {
    db.update(uptimeSpeedProbes).set({
      lastRunAt: pingNow,
      lastTxAvgMbps: txFinal,
      lastRxAvgMbps: rxFinal,
      lastStatus: "done",
      lastError: "",
      lastPingRttMs: afterBtPing.rttMs,
      lastPingLossPct: afterBtPing.lossPct,
      lastPingAt: pingNow,
      lastPingError: afterBtPing.error ?? "",
      updatedAt: pingNow,
    }).where(eq(uptimeSpeedProbes.id, probeId)).run()
  }
  db.insert(uptimeSpeedTestRuns).values({
    id: runId,
    probeId: probeId || null,
    srcServerId: srcId,
    dstServerId: dstId,
    srcInterface: srcInterface || "",
    dstInterface: dstInterface || "",
    srcAddress: srcAddress || src.host,
    dstAddress: initialAddress,
    srcInterfaceAddress,
    dstInterfaceAddress,
    protocol,
    direction,
    durationSec,
    txAvgMbps: txFinal,
    rxAvgMbps: rxFinal,
    pingRttMs: afterBtPing.rttMs,
    pingLossPct: afterBtPing.lossPct,
    pingError: afterBtPing.error ?? null,
    status: "done",
    error: null,
    createdAt: pingNow,
  }).run()

  return {
    runId,
    srcInterface: srcInterface || null,
    dstInterface: dstInterface || null,
    address: initialAddress,
    srcAddress: srcAddress || src.host,
    dstAddress: initialAddress,
    srcInterfaceAddress,
    dstInterfaceAddress,
    protocol,
    direction,
    durationSec,
    txAvgMbps: txFinal,
    rxAvgMbps: rxFinal,
    txSeries,
    rxSeries,
    raw: rows,
    afterBtPing,
  }
}

export async function runBandwidthSpeedTest(params: SpeedTestRunParams): Promise<SpeedTestRunResult> {
  const srcId = params.srcServerId
  const dstId = params.dstServerId
  if (srcId === dstId) throw new Error("Source and destination must be different")
  return withBtestNodeLocks([srcId, dstId], () => executeBandwidthSpeedTestInner(params))
}

export function recordSpeedTestFailure(params: {
  probeId?: string
  runId?: string
  srcServerId: number
  dstServerId: number
  srcInterface?: string
  dstInterface?: string
  protocol?: "tcp" | "udp"
  direction?: "transmit" | "receive" | "both"
  durationSec?: number
  message: string
}) {
  const probeId = String(params.probeId ?? "").trim()
  const message = humanizeSpeedError(params.message)
  if (probeId) {
    db.update(uptimeSpeedProbes).set({
      lastRunAt: new Date().toISOString(),
      lastStatus: "error",
      lastError: message,
      updatedAt: new Date().toISOString(),
    }).where(eq(uptimeSpeedProbes.id, probeId)).run()
  }
  db.insert(uptimeSpeedTestRuns).values({
    id: String(params.runId ?? "").trim() || `sr-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
    probeId: probeId || null,
    srcServerId: params.srcServerId,
    dstServerId: params.dstServerId,
    srcInterface: String(params.srcInterface ?? "").trim(),
    dstInterface: String(params.dstInterface ?? "").trim(),
    srcAddress: null,
    dstAddress: null,
    srcInterfaceAddress: null,
    dstInterfaceAddress: null,
    protocol: params.protocol === "udp" ? "udp" : "tcp",
    direction: params.direction === "transmit" || params.direction === "receive" ? params.direction : "both",
    durationSec: Math.max(3, Number.parseInt(String(params.durationSec ?? ""), 10) || 10),
    txAvgMbps: null,
    rxAvgMbps: null,
    pingRttMs: null,
    pingLossPct: null,
    pingError: null,
    status: "error",
    error: message,
    createdAt: new Date().toISOString(),
  }).run()
}

function serverDisplayName(id: number): string {
  const s = db.select().from(servers).where(eq(servers.id, id)).limit(1).all()[0]
  return s ? (s.name || s.host) : `#${id}`
}

/** Фоновый прогон всех включённых speed-проб (по очереди). */
export async function runScheduledSpeedProbesOnce(): Promise<SpeedScheduledRunSnapshot> {
  const sampledAt = new Date().toISOString()
  const runs: SpeedRunSnapshot[] = []
  const rows = db.select().from(uptimeSpeedProbes).orderBy(asc(uptimeSpeedProbes.sortOrder)).all()
  const enabled = rows.filter((r) => r.enabled !== false && r.srcServerId !== r.dstServerId)
  const protocol = (p: (typeof enabled)[number]) => (p.protocol === "udp" ? "udp" : "tcp")
  const direction = (p: (typeof enabled)[number]) =>
    p.direction === "transmit" || p.direction === "receive" ? p.direction : "both"
  const durationSec = (p: (typeof enabled)[number]) => Math.max(3, p.durationSec || 10)

  for (const p of enabled) {
    try {
      const r = await runBandwidthSpeedTest({
        probeId: p.id,
        srcServerId: p.srcServerId,
        dstServerId: p.dstServerId,
        srcInterface: p.srcInterface || undefined,
        dstInterface: p.dstInterface || undefined,
        protocol: protocol(p) as "tcp" | "udp",
        direction: direction(p) as "transmit" | "receive" | "both",
        durationSec: durationSec(p),
      })
      runs.push({
        probeId: p.id,
        srcServerId: p.srcServerId,
        srcServerName: serverDisplayName(p.srcServerId),
        dstServerId: p.dstServerId,
        dstServerName: serverDisplayName(p.dstServerId),
        srcInterface: r.srcInterface ?? "",
        dstInterface: r.dstInterface ?? "",
        protocol: r.protocol,
        direction: r.direction,
        durationSec: r.durationSec,
        ok: true,
        txAvgMbps: r.txAvgMbps,
        rxAvgMbps: r.rxAvgMbps,
        pingRttMs: r.afterBtPing.rttMs,
        pingLossPct: r.afterBtPing.lossPct,
        pingError: r.afterBtPing.error,
      })
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e)
      recordSpeedTestFailure({
        probeId: p.id,
        srcServerId: p.srcServerId,
        dstServerId: p.dstServerId,
        srcInterface: p.srcInterface,
        dstInterface: p.dstInterface,
        protocol: protocol(p),
        direction: direction(p),
        durationSec: p.durationSec,
        message: msg,
      })
      runs.push({
        probeId: p.id,
        srcServerId: p.srcServerId,
        srcServerName: serverDisplayName(p.srcServerId),
        dstServerId: p.dstServerId,
        dstServerName: serverDisplayName(p.dstServerId),
        srcInterface: p.srcInterface || "",
        dstInterface: p.dstInterface || "",
        protocol: protocol(p),
        direction: direction(p),
        durationSec: durationSec(p),
        ok: false,
        error: msg,
      })
    }
  }
  return {
    v: SCHEDULER_RUN_SNAPSHOT_VERSION,
    job: "uptime_speed",
    sampledAt,
    runs,
  }
}
