import { and, asc, eq, gte, inArray, lt, max, or } from "drizzle-orm"
import { db } from "../db/index.js"
import {
  servers,
  uptimeProbeSamples,
  uptimeProbes,
  uptimeResourceSamples,
  uptimeSettings,
} from "../db/schema.js"
import type { PingRunSnapshot, ResourcesRunSnapshot } from "../types/scheduler-run-snapshot.js"
import { SCHEDULER_RUN_SNAPSHOT_VERSION } from "../types/scheduler-run-snapshot.js"
import { PING_PROBE_WARN_RTT_MS } from "../constants/ping-probe.js"
import { MikrotikClient } from "./mikrotik.js"
import { parseRosCpuLoadPercent, parseRosDataSizeBytes, parseRosUptimeToSeconds } from "./ros-metric-parse.js"

let collectingResources = false
let collectingPing = false

/** Параллельный прогон сборщиков uptime — чтобы `alert_engine` не читал БД до коммита сэмплов. */
export function getUptimeCollectorsState(): { resources: boolean; ping: boolean } {
  return { resources: collectingResources, ping: collectingPing }
}

function parsePingTimeMs(raw: string | undefined): number | null {
  if (!raw) return null
  const s = String(raw).trim().toLowerCase().replace(",", ".")
  const us = s.match(/^(\d+(?:\.\d+)?)\s*us$/)
  if (us) return Number.parseFloat(us[1]) / 1000
  const ms = s.match(/^(\d+(?:\.\d+)?)\s*ms$/)
  if (ms) return Number.parseFloat(ms[1])
  const sec = s.match(/^(\d+(?:\.\d+)?)\s*s$/)
  if (sec) return Number.parseFloat(sec[1]) * 1000

  const clock = s.match(/^(\d+):(\d+):(\d+(?:\.\d+)?)$/)
  if (clock) {
    const h = Number.parseFloat(clock[1])
    const m = Number.parseFloat(clock[2])
    const sc = Number.parseFloat(clock[3])
    return (h * 3600 + m * 60 + sc) * 1000
  }

  const n = Number.parseFloat(s)
  return Number.isFinite(n) ? n : null
}

export function getSettings() {
  const row = db.select().from(uptimeSettings).where(eq(uptimeSettings.id, 1)).limit(1).all()[0]
  if (row) return row
  const now = new Date().toISOString()
  db.insert(uptimeSettings).values({
    id: 1,
    enabled: true,
    resourcesEnabled: true,
    pingEnabled: true,
    speedEnabled: true,
    intervalSec: 300,
    probeIntervalSec: 15,
    speedIntervalSec: 3600,
    retentionDays: 14,
    createdAt: now,
    updatedAt: now,
  }).run()
  return db.select().from(uptimeSettings).where(eq(uptimeSettings.id, 1)).limit(1).all()[0]
}

function cleanup(retentionDays: number) {
  const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString()
  db.delete(uptimeProbeSamples).where(lt(uptimeProbeSamples.sampledAt, cutoff)).run()
  db.delete(uptimeResourceSamples).where(lt(uptimeResourceSamples.sampledAt, cutoff)).run()
}

export function parsePing(results: Array<{ time?: string; status?: string; sent?: string; received?: string; "packet-loss"?: string; "avg-rtt"?: string }>) {
  const sum = [...results].reverse().find((r) =>
    r.sent != null || r.received != null || r["packet-loss"] != null || r["avg-rtt"] != null,
  )
  const okReplies = results.filter((r) => r.time && String(r.status ?? "").toLowerCase() !== "timeout")
  const timedOutReplies = results.filter((r) => String(r.status ?? "").toLowerCase() === "timeout")
  const rtts = okReplies
    .map((r) => parsePingTimeMs(r.time))
    .filter((v): v is number => v != null && Number.isFinite(v))
  const avgFromReplies = rtts.length ? Math.round(rtts.reduce((a, b) => a + b, 0) / rtts.length) : null
  const avgFromSummary = parsePingTimeMs(sum?.time) ?? parsePingTimeMs(sum?.["avg-rtt"])
  const avgRtt = avgFromReplies ?? (avgFromSummary != null ? Math.round(avgFromSummary) : null)
  const sent = Number.parseInt(String(sum?.sent ?? ""), 10)
  const received = Number.parseInt(String(sum?.received ?? ""), 10)
  const sentFallback = okReplies.length + timedOutReplies.length
  const safeSent = Number.isFinite(sent) && sent > 0 ? sent : sentFallback
  const safeReceived = Number.isFinite(received) && received >= 0 ? received : okReplies.length
  const computedLoss = safeSent > 0 ? Math.round(((safeSent - safeReceived) / safeSent) * 100) : null
  const lossRaw = String(sum?.["packet-loss"] ?? "")
  const parsedLoss = Number.parseInt(lossRaw.replace("%", ""), 10)
  const lossSource = Number.isFinite(parsedLoss) ? parsedLoss : computedLoss
  const loss =
    typeof lossSource === "number" && Number.isFinite(lossSource)
      ? Math.max(0, Math.min(100, lossSource))
      : 100
  const status: "up" | "warn" | "down" =
    loss >= 100 ? "down" : (loss > 1 || (avgRtt ?? 0) > PING_PROBE_WARN_RTT_MS ? "warn" : "up")
  return { avgRtt, loss, status }
}

/** Узлы с REST-опросом для uptime: включённые в каталоге или любой Home Router (см. /servers — HR часто «выкл.» при доступе только из LAN). */
function serversForUptimeRosPoll() {
  return db
    .select()
    .from(servers)
    .where(or(eq(servers.enabled, true), eq(servers.type, "home-router")))
    .all()
}

function lastProbeSampleTimes(): Map<string, string> {
  const rows = db.select({
    probeId: uptimeProbeSamples.probeId,
    lastAt: max(uptimeProbeSamples.sampledAt),
  }).from(uptimeProbeSamples).groupBy(uptimeProbeSamples.probeId).all()
  const m = new Map<string, string>()
  for (const r of rows) {
    if (r.lastAt) m.set(r.probeId, String(r.lastAt))
  }
  return m
}

/** Сбор метрик ресурсов (CPU/RAM/…) по всем включённым серверам. */
export async function collectResourceSamplesOnce(): Promise<ResourcesRunSnapshot> {
  const sampledAt = new Date().toISOString()
  if (collectingResources) {
    return {
      v: SCHEDULER_RUN_SNAPSHOT_VERSION,
      job: "uptime_resources",
      sampledAt,
      skipped: true,
      servers: [],
    }
  }
  collectingResources = true
  const started = Date.now()
  const now = sampledAt
  const settings = getSettings()
  const snapshot: ResourcesRunSnapshot = {
    v: SCHEDULER_RUN_SNAPSHOT_VERSION,
    job: "uptime_resources",
    sampledAt: now,
    servers: [],
  }

  try {
    const resourceTargets = serversForUptimeRosPoll()

    for (const s of resourceTargets) {
      const name = s.name || s.host
      try {
        const client = MikrotikClient.fromServer(s)
        const resource = await client.getResource()
        const cpuLoad = parseRosCpuLoadPercent(resource["cpu-load"])
        const freeMem = parseRosDataSizeBytes(resource["free-memory"])
        const totalMem = parseRosDataSizeBytes(resource["total-memory"])
        const freeHdd = parseRosDataSizeBytes(resource["free-hdd-space"])
        const totalHdd = parseRosDataSizeBytes(resource["total-hdd-space"])
        const uptimeSeconds = parseRosUptimeToSeconds(resource["uptime"])
        db.insert(uptimeResourceSamples).values({
          serverId: s.id,
          sampledAt: now,
          status: "online",
          cpuLoad,
          freeMemory: freeMem,
          totalMemory: totalMem,
          freeHddSpace: freeHdd,
          totalHddSpace: totalHdd,
          uptimeSeconds,
          boardName: String(resource["board-name"] ?? ""),
          rosVersion: String(resource["version"] ?? ""),
        }).run()
        const memUsedMb = totalMem > 0 ? Math.round((totalMem - freeMem) / (1024 * 1024)) : 0
        const memTotalMb = totalMem > 0 ? Math.round(totalMem / (1024 * 1024)) : 0
        snapshot.servers.push({
          serverId: s.id,
          name,
          host: s.host,
          status: "online",
          cpuLoadPct: cpuLoad,
          memUsedMb,
          memTotalMb,
          memUsedPct: totalMem > 0 ? Math.round(((totalMem - freeMem) / totalMem) * 100) : undefined,
          diskFreeMb: freeHdd > 0 ? Math.round(freeHdd / (1024 * 1024)) : 0,
          diskTotalMb: totalHdd > 0 ? Math.round(totalHdd / (1024 * 1024)) : 0,
          uptimeSeconds,
          boardName: String(resource["board-name"] ?? "") || undefined,
          rosVersion: String(resource["version"] ?? "") || undefined,
        })
      } catch (err) {
        console.warn(`[uptime-collector] resource serverId=${s.id} (${s.host}):`, err instanceof Error ? err.message : err)
        db.insert(uptimeResourceSamples).values({
          serverId: s.id,
          sampledAt: now,
          status: "offline",
          cpuLoad: 0,
          freeMemory: 0,
          totalMemory: 0,
          freeHddSpace: 0,
          totalHddSpace: 0,
          uptimeSeconds: 0,
          boardName: "",
          rosVersion: "",
        }).run()
        snapshot.servers.push({
          serverId: s.id,
          name,
          host: s.host,
          status: "offline",
          error: err instanceof Error ? err.message : String(err),
        })
      }
    }

    cleanup(Math.max(1, settings.retentionDays))
    db.update(uptimeSettings).set({
      lastCollectedAt: now,
      lastDurationMs: Date.now() - started,
      lastError: "",
      updatedAt: now,
    }).where(eq(uptimeSettings.id, 1)).run()
  } catch (e) {
    snapshot.fatalError = e instanceof Error ? e.message : String(e)
    db.update(uptimeSettings).set({
      lastCollectedAt: now,
      lastDurationMs: Date.now() - started,
      lastError: e instanceof Error ? e.message : String(e),
      updatedAt: now,
    }).where(eq(uptimeSettings.id, 1)).run()
  } finally {
    collectingResources = false
  }
  return snapshot
}

/** Ping-пробы: учитывается interval_sec у пробы или глобальный probe_interval_sec. */
export async function collectPingProbesOnce(): Promise<PingRunSnapshot> {
  const sampledAt = new Date().toISOString()
  if (collectingPing) {
    return {
      v: SCHEDULER_RUN_SNAPSHOT_VERSION,
      job: "uptime_ping",
      sampledAt,
      skipped: true,
      probes: [],
    }
  }
  collectingPing = true
  const started = Date.now()
  const now = sampledAt
  const settings = getSettings()
  const defaultProbeSec = Math.max(5, settings.probeIntervalSec ?? settings.intervalSec)
  const lastByProbe = lastProbeSampleTimes()
  const nowMs = Date.now()
  const snapshot: PingRunSnapshot = {
    v: SCHEDULER_RUN_SNAPSHOT_VERSION,
    job: "uptime_ping",
    sampledAt: now,
    probes: [],
    skippedByInterval: 0,
  }

  try {
    const rosPollServers = serversForUptimeRosPoll()
    const probes = db.select().from(uptimeProbes).where(eq(uptimeProbes.enabled, true)).orderBy(asc(uptimeProbes.sortOrder)).all()

    for (const p of probes) {
      const effectiveSec = (p.intervalSec != null && p.intervalSec > 0) ? p.intervalSec : defaultProbeSec
      const lastIso = lastByProbe.get(p.id)
      if (lastIso) {
        const elapsed = nowMs - Date.parse(lastIso)
        if (elapsed < effectiveSec * 1000) {
          snapshot.skippedByInterval = (snapshot.skippedByInterval ?? 0) + 1
          continue
        }
      }

      const src = rosPollServers.find((s) => s.id === p.srcServerId)
      if (!src) continue
      const srcServerName = src.name || src.host
      try {
        const client = MikrotikClient.fromServer(src)
        const results = await client.ping(p.target, 4, p.srcInterface || undefined)
        const parsed = parsePing(results)
        db.insert(uptimeProbeSamples).values({
          probeId: p.id,
          sampledAt: now,
          rttMs: parsed.avgRtt,
          lossPct: parsed.loss,
          status: parsed.status,
        }).run()
        snapshot.probes.push({
          probeId: p.id,
          name: p.name,
          target: p.target,
          srcServerId: p.srcServerId,
          srcServerName,
          srcInterface: p.srcInterface || "",
          ok: true,
          rttMs: parsed.avgRtt,
          lossPct: parsed.loss,
          status: parsed.status,
        })
      } catch (err) {
        db.insert(uptimeProbeSamples).values({
          probeId: p.id,
          sampledAt: now,
          rttMs: null,
          lossPct: 100,
          status: "down",
        }).run()
        snapshot.probes.push({
          probeId: p.id,
          name: p.name,
          target: p.target,
          srcServerId: p.srcServerId,
          srcServerName,
          srcInterface: p.srcInterface || "",
          ok: false,
          rttMs: null,
          lossPct: 100,
          status: "down",
          error: err instanceof Error ? err.message : String(err),
        })
      }
    }

    cleanup(Math.max(1, settings.retentionDays))
    db.update(uptimeSettings).set({
      lastCollectedAt: now,
      lastDurationMs: Date.now() - started,
      lastError: "",
      updatedAt: now,
    }).where(eq(uptimeSettings.id, 1)).run()
  } catch (e) {
    snapshot.fatalError = e instanceof Error ? e.message : String(e)
    db.update(uptimeSettings).set({
      lastCollectedAt: now,
      lastDurationMs: Date.now() - started,
      lastError: e instanceof Error ? e.message : String(e),
      updatedAt: now,
    }).where(eq(uptimeSettings.id, 1)).run()
  } finally {
    collectingPing = false
  }
  return snapshot
}

const MAX_PROBE_IDS_PER_GROUP_COLLECT = 64

/**
 * Немедленный ping по указанным пробам (без учёта interval_sec) — запись в uptime_probe_samples.
 * Используется кнопкой «Обновить» у группы «сервер + назначение» на вкладке проб.
 */
export async function collectPingForProbeIds(probeIds: string[]): Promise<{ polled: number }> {
  const uniqueIds = [...new Set(probeIds.filter((id) => typeof id === "string" && id.trim() !== ""))].slice(
    0,
    MAX_PROBE_IDS_PER_GROUP_COLLECT,
  )
  if (uniqueIds.length === 0) return { polled: 0 }

  if (collectingPing) {
    const e = new Error("Сбор ping уже выполняется, подождите")
    ;(e as Error & { statusCode?: number }).statusCode = 409
    throw e
  }

  collectingPing = true
  const now = new Date().toISOString()
  const settings = getSettings()
  let polled = 0

  try {
    const rosPollServers = serversForUptimeRosPoll()
    const rows = db.select().from(uptimeProbes).where(inArray(uptimeProbes.id, uniqueIds)).all()

    for (const p of rows) {
      const src = rosPollServers.find((s) => s.id === p.srcServerId)
      if (!src) continue
      try {
        const client = MikrotikClient.fromServer(src)
        const results = await client.ping(p.target, 4, p.srcInterface || undefined)
        const parsed = parsePing(results)
        db.insert(uptimeProbeSamples).values({
          probeId: p.id,
          sampledAt: now,
          rttMs: parsed.avgRtt,
          lossPct: parsed.loss,
          status: parsed.status,
        }).run()
      } catch {
        db.insert(uptimeProbeSamples).values({
          probeId: p.id,
          sampledAt: now,
          rttMs: null,
          lossPct: 100,
          status: "down",
        }).run()
      }
      polled += 1
    }

    cleanup(Math.max(1, settings.retentionDays))
    return { polled }
  } finally {
    collectingPing = false
  }
}

/** Совместимость: один проход ресурсы + ping (для ручного вызова без планировщика). */
export async function collectUptimeOnce(): Promise<void> {
  await collectResourceSamplesOnce()
  await collectPingProbesOnce()
}

/** no-op: таймеры управляются планировщиком */
export function restartUptimeCollector() {}

export function stopUptimeCollector() {}

export function readUptimeSettings() {
  return getSettings()
}

export function updateUptimeSettings(patch: {
  /** Устаревший: если передан без per-job флагов — выставляет все три job-флага. */
  enabled?: boolean
  resourcesEnabled?: boolean
  pingEnabled?: boolean
  speedEnabled?: boolean
  /** Интервал сбора метрик ресурсов (сек). */
  intervalSec?: number
  /** Интервал тика ping и значение по умолчанию для проб без своего interval_sec. */
  probeIntervalSec?: number
  speedIntervalSec?: number
  retentionDays?: number
}) {
  const prev = getSettings()
  const anyJobFlag =
    patch.resourcesEnabled !== undefined
    || patch.pingEnabled !== undefined
    || patch.speedEnabled !== undefined

  let resourcesEnabled = patch.resourcesEnabled ?? prev.resourcesEnabled ?? prev.enabled
  let pingEnabled = patch.pingEnabled ?? prev.pingEnabled ?? prev.enabled
  let speedEnabled = patch.speedEnabled ?? prev.speedEnabled ?? prev.enabled

  if (patch.enabled !== undefined && !anyJobFlag) {
    resourcesEnabled = patch.enabled
    pingEnabled = patch.enabled
    speedEnabled = patch.enabled
  }

  const masterEnabled = Boolean(resourcesEnabled || pingEnabled || speedEnabled)

  const next = {
    enabled: masterEnabled,
    resourcesEnabled,
    pingEnabled,
    speedEnabled,
    intervalSec: patch.intervalSec ?? prev.intervalSec,
    probeIntervalSec: patch.probeIntervalSec ?? prev.probeIntervalSec,
    speedIntervalSec: patch.speedIntervalSec ?? prev.speedIntervalSec ?? 60,
    retentionDays: patch.retentionDays ?? prev.retentionDays,
    updatedAt: new Date().toISOString(),
  }
  db.update(uptimeSettings).set(next).where(eq(uptimeSettings.id, 1)).run()
  return readUptimeSettings()
}

export function readProbeRows() {
  return db.select().from(uptimeProbes).orderBy(asc(uptimeProbes.sortOrder)).all()
}

export function updateProbeShowOnDashboard(probeId: string, showOnDashboard: boolean): boolean {
  const now = new Date().toISOString()
  const r = db.update(uptimeProbes)
    .set({ showOnDashboard, updatedAt: now })
    .where(eq(uptimeProbes.id, probeId))
    .run()
  return (r.changes ?? 0) > 0
}

export function replaceProbes(rows: Array<{
  id: string
  srcServerId: number
  srcInterface: string
  name: string
  target: string
  probeFilter: string
  enabled: boolean
  showOnDashboard?: boolean
  intervalSec?: number
}>) {
  const now = new Date().toISOString()
  const existing = db.select({ id: uptimeProbes.id }).from(uptimeProbes).all()
  const nextIds = new Set(rows.map((r) => r.id))

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

  for (let i = 0; i < rows.length; i += 1) {
    const r = rows[i]
    const intervalSec = r.intervalSec != null && r.intervalSec > 0 ? Math.max(5, Math.floor(r.intervalSec)) : 0
    const patch = {
      srcServerId: r.srcServerId,
      srcInterface: r.srcInterface || "",
      name: r.name,
      target: r.target,
      probeFilter: r.probeFilter || "—",
      enabled: r.enabled,
      showOnDashboard: r.showOnDashboard ?? false,
      intervalSec,
      sortOrder: i,
      updatedAt: now,
    }
    const updated = db.update(uptimeProbes).set(patch).where(eq(uptimeProbes.id, r.id)).run()
    if ((updated.changes ?? 0) > 0) continue
    db.insert(uptimeProbes).values({
      id: r.id,
      ...patch,
      createdAt: now,
    }).run()
  }
}

export function readProbeSamplesSince(sinceIso: string) {
  return db.select().from(uptimeProbeSamples)
    .where(gte(uptimeProbeSamples.sampledAt, sinceIso))
    .orderBy(asc(uptimeProbeSamples.sampledAt))
    .all()
}

export function readResourceSamplesSince(sinceIso: string, serverId: number) {
  return db.select().from(uptimeResourceSamples)
    .where(and(
      eq(uptimeResourceSamples.serverId, serverId),
      gte(uptimeResourceSamples.sampledAt, sinceIso),
    ))
    .orderBy(asc(uptimeResourceSamples.sampledAt))
    .all()
}
