import { randomUUID } from "node:crypto"
import { mkdir, rm, stat, writeFile } from "node:fs/promises"
import path from "node:path"
import { desc, eq } from "drizzle-orm"
import type { BackupScheduleSettingsDto } from "@mmapp/contracts/backups"
import { db } from "../db/index.js"
import { backupEntries, backupScheduleSettings } from "../db/schema.js"
import { getServerRowById } from "../modules/servers/repository/servers-repository.js"
import { listServersRead } from "../modules/servers/service/servers-service.js"
import { MikrotikClient } from "./mikrotik.js"

const SETTINGS_ID = 1
const BACKUPS_DIR = path.resolve(process.cwd(), "storage", "backups")

export type BackupMeta = {
  id: string
  serverId: string
  serverName: string
  filename: string
  sizeBytes: number
  createdAt: string
  kind: "manual" | "auto"
  notes?: string
}

function rowToMeta(row: typeof backupEntries.$inferSelect): BackupMeta {
  return {
    id: row.id,
    serverId: row.serverId,
    serverName: row.serverName,
    filename: row.filename,
    sizeBytes: row.sizeBytes,
    createdAt: row.createdAt,
    kind: row.kind,
    notes: row.notes ?? undefined,
  }
}

export async function ensureBackupStorage(): Promise<void> {
  await mkdir(BACKUPS_DIR, { recursive: true })
}

export function listBackups(): BackupMeta[] {
  return db.select().from(backupEntries).orderBy(desc(backupEntries.createdAt)).all().map(rowToMeta)
}

export function getBackupById(id: string): BackupMeta | null {
  const row = db.select().from(backupEntries).where(eq(backupEntries.id, id)).limit(1).all()[0]
  return row ? rowToMeta(row) : null
}

export function insertBackup(meta: BackupMeta): void {
  db.insert(backupEntries).values({
    id: meta.id,
    serverId: meta.serverId,
    serverName: meta.serverName,
    filename: meta.filename,
    sizeBytes: meta.sizeBytes,
    kind: meta.kind,
    notes: meta.notes ?? null,
    createdAt: meta.createdAt,
  }).run()
}

export async function deleteBackupRecord(id: string): Promise<BackupMeta | null> {
  const hit = getBackupById(id)
  if (!hit) return null
  db.delete(backupEntries).where(eq(backupEntries.id, id)).run()
  await rm(path.join(BACKUPS_DIR, hit.filename), { force: true })
  return hit
}

function fmtTs(d = new Date()): string {
  const p = (n: number) => String(n).padStart(2, "0")
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}_${p(d.getHours())}-${p(d.getMinutes())}-${p(d.getSeconds())}`
}

function parseServerIds(raw: string): string[] {
  try {
    const parsed = JSON.parse(raw) as unknown
    if (!Array.isArray(parsed)) return []
    return parsed.map(String).filter(Boolean)
  } catch {
    return []
  }
}

function getBackupScheduleSettingsRow() {
  return db.select().from(backupScheduleSettings).where(eq(backupScheduleSettings.id, SETTINGS_ID)).limit(1).all()[0]
    ?? {
      id: SETTINGS_ID,
      enabled: true,
      frequency: "daily" as const,
      hour: 3,
      minute: 0,
      weekDay: 0,
      monthDay: 1,
      keepCount: 7,
      format: "rsc" as const,
      serverIdsJson: "[]",
      lastRunAt: null,
      lastDurationMs: null,
      lastError: null,
      updatedAt: new Date().toISOString(),
    }
}

export function getBackupScheduleSettings(): BackupScheduleSettingsDto {
  const row = getBackupScheduleSettingsRow()
  return {
    enabled: row.enabled,
    frequency: row.frequency,
    hour: row.hour,
    minute: row.minute,
    weekDay: row.weekDay,
    monthDay: row.monthDay,
    keepCount: row.keepCount,
    format: row.format,
    serverIds: parseServerIds(row.serverIdsJson),
    lastRunAt: row.lastRunAt ?? null,
    lastDurationMs: row.lastDurationMs ?? null,
    lastError: row.lastError ?? null,
    updatedAt: row.updatedAt,
  }
}

export function updateBackupScheduleSettings(patch: Partial<{
  enabled: boolean
  frequency: "daily" | "weekly" | "monthly"
  hour: number
  minute: number
  weekDay: number
  monthDay: number
  keepCount: number
  format: "rsc" | "backup"
  serverIds: string[]
}>) {
  const prev = getBackupScheduleSettingsRow()
  const now = new Date().toISOString()
  const next = {
    enabled: patch.enabled ?? prev.enabled,
    frequency: patch.frequency ?? prev.frequency,
    hour: patch.hour ?? prev.hour,
    minute: patch.minute ?? prev.minute,
    weekDay: patch.weekDay ?? prev.weekDay,
    monthDay: patch.monthDay ?? prev.monthDay,
    keepCount: patch.keepCount ?? prev.keepCount,
    format: patch.format ?? prev.format,
    serverIdsJson: patch.serverIds ? JSON.stringify(patch.serverIds) : prev.serverIdsJson,
    updatedAt: now,
  }
  if (db.select().from(backupScheduleSettings).where(eq(backupScheduleSettings.id, SETTINGS_ID)).limit(1).all()[0]) {
    db.update(backupScheduleSettings).set(next).where(eq(backupScheduleSettings.id, SETTINGS_ID)).run()
  } else {
    db.insert(backupScheduleSettings).values({ id: SETTINGS_ID, ...next }).run()
  }
  return getBackupScheduleSettings()
}

export function touchBackupScheduleRunMeta(patch: {
  lastRunAt?: string
  lastDurationMs?: number
  lastError?: string | null
}) {
  const prev = getBackupScheduleSettingsRow()
  db.update(backupScheduleSettings).set({
    lastRunAt: patch.lastRunAt ?? prev.lastRunAt,
    lastDurationMs: patch.lastDurationMs ?? prev.lastDurationMs,
    lastError: patch.lastError === undefined ? prev.lastError : patch.lastError,
    updatedAt: new Date().toISOString(),
  }).where(eq(backupScheduleSettings.id, SETTINGS_ID)).run()
}

export function resolveBackupServerIds(settings: BackupScheduleSettingsDto): string[] {
  const enabled = new Set(listServersRead().map((s) => String(s.id)))
  const requested = settings.serverIds.length > 0 ? settings.serverIds : [...enabled]
  return [...new Set(requested)].filter((id) => enabled.has(id))
}

function scheduledSlotForDate(now: Date, settings: BackupScheduleSettingsDto): Date | null {
  if (settings.frequency === "weekly") {
    const currentDow = (now.getDay() + 6) % 7
    if (currentDow !== settings.weekDay) return null
  } else if (settings.frequency === "monthly") {
    if (now.getDate() !== settings.monthDay) return null
  }

  const slot = new Date(now)
  slot.setSeconds(0, 0)
  slot.setMilliseconds(0)
  slot.setHours(settings.hour, settings.minute, 0, 0)
  return slot
}

function sameLocalMinute(a: Date, b: Date): boolean {
  return a.getFullYear() === b.getFullYear()
    && a.getMonth() === b.getMonth()
    && a.getDate() === b.getDate()
    && a.getHours() === b.getHours()
    && a.getMinutes() === b.getMinutes()
}

/** Срабатывает только в минуту расписания; 60 с в UI — интервал проверки, не частота бэкапа. */
export function isBackupDue(now: Date, settings: BackupScheduleSettingsDto, lastRunAt: string | null | undefined): boolean {
  if (!settings.enabled) return false

  const slot = scheduledSlotForDate(now, settings)
  if (!slot) return false
  if (now < slot) return false
  if (!sameLocalMinute(now, slot)) return false

  if (lastRunAt) {
    const prev = new Date(lastRunAt)
    if (Number.isNaN(prev.getTime())) return true
    if (sameLocalMinute(prev, slot)) return false
  }

  return true
}

export async function runBackupForServer(
  id: string,
  kind: BackupMeta["kind"],
  notes?: string,
): Promise<BackupMeta> {
  const serverIdNum = Number.parseInt(id, 10)
  if (!Number.isFinite(serverIdNum)) {
    throw new Error("Невалидный id сервера")
  }
  const row = getServerRowById(serverIdNum)
  if (!row) {
    throw new Error("Сервер не найден")
  }
  const client = MikrotikClient.fromServer(row)
  const script = await client.exportConfigScript()
  const ts = fmtTs()
  const safeServer = row.name.replace(/[^a-zA-Z0-9._-]+/g, "_")
  const filename = `${safeServer}_${ts}.rsc`
  const filePath = path.join(BACKUPS_DIR, filename)
  await ensureBackupStorage()
  await writeFile(filePath, script, "utf8")
  const st = await stat(filePath)
  const meta: BackupMeta = {
    id: randomUUID(),
    serverId: String(row.id),
    serverName: row.name,
    filename,
    sizeBytes: st.size,
    createdAt: new Date().toISOString(),
    kind,
    notes,
  }
  insertBackup(meta)
  return meta
}

export async function pruneBackupsForServer(serverId: string, keepCount: number): Promise<number> {
  const rows = db.select().from(backupEntries)
    .where(eq(backupEntries.serverId, serverId))
    .orderBy(desc(backupEntries.createdAt))
    .all()
  if (rows.length <= keepCount) return 0
  const toDelete = rows.slice(keepCount)
  for (const hit of toDelete) {
    db.delete(backupEntries).where(eq(backupEntries.id, hit.id)).run()
    await rm(path.join(BACKUPS_DIR, hit.filename), { force: true })
  }
  return toDelete.length
}

export function getBackupsDir(): string {
  return BACKUPS_DIR
}
