import { randomUUID } from "node:crypto"
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"
import path from "node:path"
import { z } from "zod"
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { listServersRead } from "../modules/servers/service/servers-service.js"
import { getServerRowById } from "../modules/servers/repository/servers-repository.js"
import { MikrotikClient } from "../services/mikrotik.js"
import { appendEvent } from "../modules/events/service/events-service.js"

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

type BackupJobStatus = "queued" | "running" | "done" | "failed"
type BackupJob = {
  id: string
  status: BackupJobStatus
  requestedAt: string
  startedAt?: string
  finishedAt?: string
  total: number
  completed: number
  created: BackupMeta[]
  failures: Array<{ serverId: string; error: string }>
}

const BACKUPS_DIR = path.resolve(process.cwd(), "storage", "backups")
const INDEX_PATH = path.join(BACKUPS_DIR, "index.json")
const backupJobs = new Map<string, BackupJob>()

async function ensureStorage() {
  await mkdir(BACKUPS_DIR, { recursive: true })
}

async function readIndex(): Promise<BackupMeta[]> {
  await ensureStorage()
  try {
    const raw = await readFile(INDEX_PATH, "utf8")
    const parsed = JSON.parse(raw) as unknown
    if (!Array.isArray(parsed)) return []
    return parsed as BackupMeta[]
  } catch {
    return []
  }
}

async function writeIndex(rows: BackupMeta[]): Promise<void> {
  await ensureStorage()
  await writeFile(INDEX_PATH, JSON.stringify(rows, null, 2), "utf8")
}

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())}`
}

const CreateBackupBodySchema = z.object({
  serverIds: z.array(z.union([z.string(), z.number()])).min(1),
  notes: z.string().max(500).optional(),
})

const BackupIdParamSchema = z.object({
  id: z.string().min(1),
})
const BackupJobIdParamSchema = z.object({
  jobId: z.string().min(1),
})

async function runBackupForServer(id: string, 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 writeFile(filePath, script, "utf8")
  const st = await stat(filePath)
  return {
    id: randomUUID(),
    serverId: String(row.id),
    serverName: row.name,
    filename,
    sizeBytes: st.size,
    createdAt: new Date().toISOString(),
    kind: "manual",
    notes,
  }
}

async function processBackupJob(job: BackupJob, ids: string[], notes?: string) {
  job.status = "running"
  job.startedAt = new Date().toISOString()
  const indexRows = await readIndex()
  for (const id of ids) {
    try {
      const meta = await runBackupForServer(id, notes)
      indexRows.unshift(meta)
      job.created.push(meta)
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err)
      job.failures.push({ serverId: id, error: message })
    } finally {
      job.completed += 1
    }
  }
  await writeIndex(indexRows)
  job.status = "done"
  job.finishedAt = new Date().toISOString()
  appendEvent({
    level: job.failures.length > 0 ? "warning" : "info",
    eventType: "backups.job.done",
    sourceModule: "backups",
    title: job.failures.length > 0 ? "Бэкап завершен с ошибками" : "Бэкап завершен",
    message: `Создано: ${job.created.length}, ошибок: ${job.failures.length}`,
    entityType: "backup_job",
    entityId: job.id,
    payload: {
      total: job.total,
      completed: job.completed,
      failures: job.failures,
      notes: notes ?? null,
    },
  })
}

const backupsRoutes: FastifyPluginAsyncZod = async (app) => {
  app.get("/backups", async (_req, reply) => {
    const rows = await readIndex()
    rows.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
    return reply.send(rows)
  })

  app.post("/backups/create", { schema: { body: CreateBackupBodySchema } }, async (req, reply) => {
    const inputIds = req.body.serverIds.map((x) => String(x))
    const notes = req.body.notes?.trim() || undefined
    const existingServers = new Set(listServersRead().map((s) => String(s.id)))
    const ids = [...new Set(inputIds)].filter((id) => existingServers.has(id))
    if (ids.length === 0) return reply.status(400).send({ error: "Не выбраны валидные серверы" })

    const jobId = randomUUID()
    const job: BackupJob = {
      id: jobId,
      status: "queued",
      requestedAt: new Date().toISOString(),
      total: ids.length,
      completed: 0,
      created: [],
      failures: [],
    }
    backupJobs.set(jobId, job)
    appendEvent({
      level: "info",
      eventType: "backups.job.started",
      sourceModule: "backups",
      title: "Запущен бэкап",
      message: `Серверов в очереди: ${ids.length}`,
      entityType: "backup_job",
      entityId: jobId,
      payload: {
        serverIds: ids,
        notes: notes ?? null,
      },
    })
    queueMicrotask(() => {
      void processBackupJob(job, ids, notes).catch((err) => {
        job.status = "failed"
        job.finishedAt = new Date().toISOString()
        const failure = {
          serverId: "job",
          error: err instanceof Error ? err.message : String(err),
        }
        job.failures.push(failure)
        appendEvent({
          level: "critical",
          eventType: "backups.job.failed",
          sourceModule: "backups",
          title: "Бэкап прерван",
          message: failure.error,
          entityType: "backup_job",
          entityId: job.id,
          payload: {
            total: job.total,
            completed: job.completed,
          },
        })
      })
    })
    return reply.status(202).send({
      jobId,
      status: job.status,
      total: job.total,
      completed: job.completed,
    })
  })

  app.get("/backups/jobs/:jobId", { schema: { params: BackupJobIdParamSchema } }, async (req, reply) => {
    const job = backupJobs.get(req.params.jobId)
    if (!job) return reply.status(404).send({ error: "Job не найден" })
    return reply.send(job)
  })

  app.get("/backups/:id/download", { schema: { params: BackupIdParamSchema } }, async (req, reply) => {
    const rows = await readIndex()
    const hit = rows.find((r) => r.id === req.params.id)
    if (!hit) return reply.status(404).send({ error: "Бэкап не найден" })
    const filePath = path.join(BACKUPS_DIR, hit.filename)
    const content = await readFile(filePath, "utf8").catch(() => null)
    if (content == null) return reply.status(404).send({ error: "Файл бэкапа не найден" })
    reply.header("Content-Type", "text/plain; charset=utf-8")
    reply.header("Content-Disposition", `attachment; filename="${hit.filename}"`)
    return reply.send(content)
  })

  app.delete("/backups/:id", { schema: { params: BackupIdParamSchema } }, async (req, reply) => {
    const rows = await readIndex()
    const idx = rows.findIndex((r) => r.id === req.params.id)
    if (idx < 0) return reply.status(404).send({ error: "Бэкап не найден" })
    const [hit] = rows.splice(idx, 1)
    await writeIndex(rows)
    await rm(path.join(BACKUPS_DIR, hit.filename), { force: true })
    return reply.status(204).send()
  })
}

export default backupsRoutes
