import { randomUUID } from "node:crypto"
import { readFile } from "node:fs/promises"
import path from "node:path"
import { z } from "zod"
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { putBackupScheduleSettingsSchema } from "@mmapp/contracts/backups"
import { listServersRead } from "../modules/servers/service/servers-service.js"
import { appendEvent } from "../modules/events/service/events-service.js"
import { refreshScheduler } from "../services/scheduler.js"
import {
  deleteBackupRecord,
  getBackupById,
  getBackupsDir,
  getBackupScheduleSettings,
  listBackups,
  runBackupForServer,
  updateBackupScheduleSettings,
  type BackupMeta,
} from "../services/backup-service.js"

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 backupJobs = new Map<string, BackupJob>()

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 processBackupJob(job: BackupJob, ids: string[], notes?: string) {
  job.status = "running"
  job.startedAt = new Date().toISOString()
  for (const id of ids) {
    try {
      const meta = await runBackupForServer(id, "manual", notes)
      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
    }
  }
  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) => {
    return reply.send(listBackups())
  })

  app.get("/backups/schedule", async (_req, reply) => {
    return reply.send(getBackupScheduleSettings())
  })

  app.put("/backups/schedule", async (req, reply) => {
    const parsed = putBackupScheduleSettingsSchema.safeParse(req.body ?? {})
    if (!parsed.success) {
      return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
    }
    const result = updateBackupScheduleSettings(parsed.data)
    refreshScheduler()
    return reply.send(result)
  })

  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 hit = getBackupById(req.params.id)
    if (!hit) return reply.status(404).send({ error: "Бэкап не найден" })
    const filePath = path.join(getBackupsDir(), 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 hit = await deleteBackupRecord(req.params.id)
    if (!hit) return reply.status(404).send({ error: "Бэкап не найден" })
    return reply.status(204).send()
  })
}

export default backupsRoutes
