import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import {
  ServerCreateSchema,
  ServerUpdateSchema,
  ServerIdParamSchema,
  SnapshotsQuerySchema,
  TestConnectionSchema,
  type ServerCreateRequest,
  type ServerIdParams,
  type ServerUpdateRequest,
  type SnapshotsQuery,
  type TestConnectionRequest,
} from "../types/server.js"
import { pollServer } from "../services/poller.js"
import { MikrotikClient, MikrotikError } from "../services/mikrotik.js"
import { resolveRosSrcIpv4 } from "../utils/ros-src-address.js"
import {
  createServer,
  deleteServer,
  getServerReadById,
  listServerSnapshots,
  listServersRead,
  updateServer,
} from "../modules/servers/service/servers-service.js"
import { getServerRowById } from "../modules/servers/repository/servers-repository.js"

// ── plugin ─────────────────────────────────────────────────────────────────────

const serversRoutes: FastifyPluginAsyncZod = async (app) => {

  // POST /api/servers/test-connection — check credentials before saving
  app.post("/test-connection", { schema: { body: TestConnectionSchema } }, async (req, reply) => {
    const { host, port, useSsl, verifySsl, apiPath, username, password } = req.body as TestConnectionRequest
    const client = new MikrotikClient({ host, port, useSsl, verifySsl, apiPath, username, password })

    try {
      const t0 = performance.now()
      const [identity, resource] = await Promise.all([
        client.getIdentity(),
        client.getResource(),
      ])
      const latencyMs = Math.round(performance.now() - t0)

      return reply.send({
        success:    true,
        latencyMs,
        identity:   identity.name,
        version:    resource["version"],
        boardName:  resource["board-name"],
        uptime:     resource["uptime"],
        message:    `Подключено · RouterOS ${resource["version"]} · ${resource["board-name"]}`,
      })
    } catch (err) {
      let message = "Не удалось подключиться"

      if (err instanceof MikrotikError) {
        if (err.statusCode === 401) message = "Неверный логин или пароль (401 Unauthorized)"
        else if (err.statusCode === 403) message = "Нет прав доступа к REST API (403 Forbidden)"
        else message = `Ошибка RouterOS API: ${err.statusCode}`
      } else if (err instanceof Error) {
        const msg = err.message.toLowerCase()
        if (msg.includes("timeout") || msg.includes("abort"))
          message = `Нет ответа от ${host}:${port} — проверьте IP и что сервис www/www-ssl включён`
        else if (msg.includes("econnrefused"))
          message = `Соединение отклонено — порт ${port} закрыт`
        else if (msg.includes("cert") || msg.includes("ssl") || msg.includes("tls"))
          message = "Ошибка SSL-сертификата — отключите «Проверять SSL» для self-signed"
        else if (msg.includes("enotfound") || msg.includes("getaddrinfo"))
          message = `Хост не найден: ${host}`
        else
          message = err.message
      }

      return reply.status(502).send({ success: false, message })
    }
  })

  // GET /api/servers
  app.get("/", async (_req, reply) => {
    return reply.send(listServersRead())
  })

  // GET /api/servers/:id/ros-src-address — IPv4 для src-address в RouterOS (не FQDN)
  app.get("/:id/ros-src-address", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
    const params = req.params as ServerIdParams
    const server = getServerRowById(params.id)

    if (!server) return reply.status(404).send({ error: "Server not found" })
    const ipv4 = await resolveRosSrcIpv4(server.host)
    return reply.send({ host: server.host, ipv4 })
  })

  // GET /api/servers/:id/wan-runtime — DHCP lease + active default route for HomeRouter WAN uplinks
  app.get("/:id/wan-runtime", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
    const params = req.params as ServerIdParams
    const server = getServerReadById(params.id)
    if (!server) return reply.status(404).send({ error: "Server not found" })

    const toIp = (raw: string | null | undefined): string | null => {
      const v = String(raw ?? "").trim()
      if (!v) return null
      return v.split("/")[0]?.trim() ?? null
    }
    const ipv4ToUint = (ip: string): number | null => {
      const p = ip.split(".").map((x) => Number.parseInt(x, 10))
      if (p.length !== 4 || p.some((x) => !Number.isFinite(x) || x < 0 || x > 255)) return null
      return (((p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]) >>> 0)
    }
    const maskFromLen = (len: number): number => {
      if (len <= 0) return 0
      if (len >= 32) return 0xffffffff
      return (~((1 << (32 - len)) - 1)) >>> 0
    }
    const parseDstRoute = (dst: string): { net: number; maskBits: number } | null => {
      const t = dst.trim()
      if (!t) return null
      if (!t.includes("/")) {
        const ip = ipv4ToUint(t)
        return ip == null ? null : { net: ip, maskBits: 32 }
      }
      const [addr, mb] = t.split("/")
      const ip = ipv4ToUint(addr.trim())
      const maskBits = Number.parseInt((mb ?? "").trim(), 10)
      if (ip == null || !Number.isFinite(maskBits) || maskBits < 0 || maskBits > 32) return null
      const mask = maskFromLen(maskBits)
      return { net: ip & mask, maskBits }
    }
    const routeHasIp = (routeDst: string, ip: string): boolean => {
      const ipu = ipv4ToUint(ip)
      const cidr = parseDstRoute(routeDst)
      if (ipu == null || !cidr) return false
      const mask = maskFromLen(cidr.maskBits)
      return (ipu & mask) === (cidr.net & mask)
    }
    const gatewayIface = (gw: string | null | undefined): string | null => {
      const v = String(gw ?? "").trim()
      if (!v) return null
      const idx = v.indexOf("%")
      if (idx < 0) return null
      return v.slice(idx + 1).trim() || null
    }

    try {
      const client = MikrotikClient.fromServer(getServerRowById(params.id)!)
      const isTrue = (v: unknown) => {
        const s = String(v ?? "").trim().toLowerCase()
        return s === "true" || s === "yes"
      }
      const [dhcpRaw, ipAddrs, routes] = await Promise.all([
        client.get<Array<Record<string, string>>>("/ip/dhcp-client").catch(() => []),
        client.getIpAddresses().catch(() => []),
        client.getIpRoutes().catch(() => []),
      ])

      const defaultRoute = routes
        .filter((r) => String(r["dst-address"] ?? "").trim() === "0.0.0.0/0")
        // Только реально ACTIVE default routes.
        .filter((r) => isTrue((r as unknown as Record<string, unknown>).active))
        // Эквивалент CLI: routing-table=main
        .filter((r) => {
          const rt = String((r as unknown as Record<string, unknown>)["routing-table"] ?? "").trim().toLowerCase()
          return rt === "" || rt === "main"
        })
        .filter((r) => String((r as unknown as Record<string, unknown>).disabled ?? "false").toLowerCase() !== "true")
        .filter((r) => String((r as unknown as Record<string, unknown>).inactive ?? "false").toLowerCase() !== "true")
        .sort((a, b) => Number(a.distance ?? 255) - Number(b.distance ?? 255))[0]

      const defaultGateway = String(defaultRoute?.gateway ?? "").trim() || null
      const immediateGw = String((defaultRoute as unknown as Record<string, unknown> | undefined)?.["immediate-gw"] ?? "").trim() || null
      const gwFromImmediate = immediateGw ? immediateGw.split("%")[0]?.trim() ?? null : null
      const gwIp = toIp(defaultGateway) ?? gwFromImmediate
      const dhcpIfaceByGateway = gwIp == null
        ? null
        : (
          dhcpRaw.find((d) => {
            const status = String(d.status ?? "").trim().toLowerCase()
            if (status && status !== "bound") return false
            return toIp(d.gateway) === gwIp
          })
        )
      const directIfaceByGwSubnet =
        gwIp == null
          ? null
          : (
            routes
              .filter((r) => String((r as unknown as Record<string, unknown>).active ?? "").toLowerCase() === "true")
              .filter((r) => String((r as unknown as Record<string, unknown>)["dst-address"] ?? "").trim() !== "0.0.0.0/0")
              .filter((r) => String((r as unknown as Record<string, unknown>).disabled ?? "false").toLowerCase() !== "true")
              .filter((r) => String((r as unknown as Record<string, unknown>).inactive ?? "false").toLowerCase() !== "true")
              .find((r) => routeHasIp(String((r as unknown as Record<string, unknown>)["dst-address"] ?? ""), gwIp))
          )
      const defaultInterface =
        gatewayIface(immediateGw)
        || gatewayIface(defaultGateway)
        || String((dhcpIfaceByGateway as unknown as Record<string, unknown> | undefined)?.interface ?? "").trim()
        || String(defaultRoute?.interface ?? "").trim()
        || String((directIfaceByGwSubnet as unknown as Record<string, unknown> | undefined)?.interface ?? "").trim()
        || null

      const uplinks = (server.wanUplinks ?? []).map((w) => {
        const iface = String(w.iface ?? "").trim()
        const dhcp = dhcpRaw.find((d) => String(d.interface ?? "").trim() === iface)
        const fromDhcp = toIp(dhcp?.address)
        const fromIpAddr = toIp(ipAddrs.find((a) => String(a.interface ?? "").trim() === iface)?.address)
        const leasedIp = fromDhcp ?? fromIpAddr
        return {
          id: w.id,
          iface,
          name: w.name,
          isp: w.isp,
          configuredIp: w.ip,
          leasedIp,
          dhcpStatus: String(dhcp?.status ?? "").trim() || null,
          isDefault: defaultInterface != null && defaultInterface === iface,
        }
      })

      return reply.send({
        defaultGateway,
          immediateGateway: immediateGw,
        defaultInterface,
        uplinks,
      })
    } catch (err) {
      return reply.status(502).send({ error: err instanceof Error ? err.message : "Failed to read WAN runtime" })
    }
  })

  // POST /api/servers
  app.post("/", { schema: { body: ServerCreateSchema } }, async (req, reply) => {
    return reply.status(201).send(createServer(req.body as ServerCreateRequest))
  })

  // GET /api/servers/:id
  app.get("/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
    const params = req.params as ServerIdParams
    const server = getServerReadById(params.id)
    if (!server) return reply.status(404).send({ error: "Server not found" })
    return reply.send(server)
  })

  // PUT /api/servers/:id
  app.put(
    "/:id",
    { schema: { params: ServerIdParamSchema, body: ServerUpdateSchema } },
    async (req, reply) => {
      const params = req.params as ServerIdParams
      const existing = getServerReadById(params.id)
      if (!existing) return reply.status(404).send({ error: "Server not found" })
      return reply.send(updateServer(params.id, req.body as ServerUpdateRequest))
    },
  )

  // DELETE /api/servers/:id
  app.delete("/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
    const params = req.params as ServerIdParams
    const existing = getServerReadById(params.id)
    if (!existing) return reply.status(404).send({ error: "Server not found" })
    deleteServer(params.id)
    return reply.status(204).send()
  })

  // POST /api/servers/:id/poll
  app.post("/:id/poll", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
    const params = req.params as ServerIdParams
    const existing = getServerReadById(params.id)
    if (!existing) return reply.status(404).send({ error: "Server not found" })

    try {
      const snap = await pollServer(params.id)
      return reply.send(snap)
    } catch (err) {
      return reply.status(500).send({ error: (err as Error).message })
    }
  })

  // GET /api/servers/:id/snapshots
  app.get(
    "/:id/snapshots",
    { schema: { params: ServerIdParamSchema, querystring: SnapshotsQuerySchema } },
    async (req, reply) => {
      const params = req.params as ServerIdParams
      const query = req.query as SnapshotsQuery
      const existing = getServerReadById(params.id)
      if (!existing) return reply.status(404).send({ error: "Server not found" })
      return reply.send(listServerSnapshots(params.id, query.limit))
    },
  )
}

export default serversRoutes
