import { lookup } from "node:dns/promises"
import * as acme from "acme-client"
import type { Server } from "../db/schema.js"
import {
  getAcmeAccountPrivateKey,
  getAcmeCloudflareToken,
  getAcmeSettingsPublic,
  saveAcmeAccountPrivateKey,
} from "./certificates-service.js"
import { MikrotikClient } from "./mikrotik.js"

type CfResponse<T> = { success: boolean; errors?: Array<{ message?: string }>; result?: T }

type CfDnsRecord = {
  id: string
  type: string
  name: string
  content: string
}

type WanUplinkRow = {
  iface?: string
  ip?: string
}

async function cloudflareRequest<T>(
  token: string,
  path: string,
  init?: RequestInit,
): Promise<T> {
  const headers: Record<string, string> = {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  }
  const res = await fetch(`https://api.cloudflare.com/client/v4${path}`, {
    ...init,
    headers,
  })
  const json = (await res.json()) as CfResponse<T>
  if (!res.ok || !json.success) {
    const msg = json.errors?.map((e) => e.message).filter(Boolean).join("; ")
      || `Cloudflare API HTTP ${res.status}`
    throw new Error(msg)
  }
  return json.result as T
}

export async function testCloudflareToken(token: string): Promise<void> {
  await cloudflareRequest<Array<{ id: string; name: string }>>(token, "/zones?per_page=1")
}

async function resolveZoneId(token: string, domain: string, defaultZoneId?: string): Promise<string> {
  const labels = domain.split(".").filter(Boolean)
  for (let i = 0; i < labels.length - 1; i++) {
    const guess = labels.slice(i).join(".")
    const zones = await cloudflareRequest<Array<{ id: string; name: string }>>(
      token,
      `/zones?name=${encodeURIComponent(guess)}`,
    )
    if (zones[0]?.id) return zones[0].id
  }
  if (defaultZoneId?.trim()) return defaultZoneId.trim()
  throw new Error(`Не удалось определить зону Cloudflare для ${domain}`)
}

async function createTxtRecord(
  token: string,
  zoneId: string,
  recordName: string,
  value: string,
): Promise<string> {
  const created = await cloudflareRequest<{ id: string }>(token, `/zones/${zoneId}/dns_records`, {
    method: "POST",
    body: JSON.stringify({
      type: "TXT",
      name: recordName,
      content: value,
      ttl: 120,
    }),
  })
  return created.id
}

async function deleteTxtRecord(token: string, zoneId: string, recordId: string): Promise<void> {
  await cloudflareRequest(token, `/zones/${zoneId}/dns_records/${recordId}`, { method: "DELETE" })
}

function parseWanUplinks(raw: string | null | undefined): WanUplinkRow[] {
  if (!raw?.trim()) return []
  try {
    const data = JSON.parse(raw) as unknown
    if (!Array.isArray(data)) return []
    return data.filter((row): row is WanUplinkRow => row != null && typeof row === "object")
  } catch {
    return []
  }
}

function normalizeIpv4(raw: string | undefined): string | null {
  const value = raw?.trim()
  if (!value) return null
  const host = value.split("/")[0]?.trim() ?? ""
  if (!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) return null
  return host
}

function isPrivateIpv4(ip: string): boolean {
  const [a, b] = ip.split(".").map((part) => Number.parseInt(part, 10))
  if (a === 10) return true
  if (a === 127) return true
  if (a === 192 && b === 168) return true
  if (a === 172 && b >= 16 && b <= 31) return true
  return false
}

async function resolveHostIpv4(host: string): Promise<string | null> {
  const literal = normalizeIpv4(host)
  if (literal) return literal
  const value = host.trim()
  if (!value) return null
  try {
    const result = await lookup(value, { family: 4 })
    return normalizeIpv4(result.address)
  } catch {
    return null
  }
}

async function resolveServerPublicIp(server: Server, client?: MikrotikClient): Promise<string> {
  const uplinks = parseWanUplinks(server.wanUplinks)
  for (const uplink of uplinks) {
    const configuredIp = normalizeIpv4(uplink.ip)
    if (configuredIp) return configuredIp
  }

  const hostIp = await resolveHostIpv4(server.host)
  if (hostIp) return hostIp

  const rosClient = client ?? MikrotikClient.fromServer(server)
  const addresses = await rosClient.getIpAddresses()
  const ifaceNames = new Set(uplinks.map((u) => u.iface?.trim()).filter(Boolean))
  const preferred = addresses.filter((row) => ifaceNames.has(String(row.interface ?? "").trim()))
  const ordered = [...preferred, ...addresses]

  for (const row of ordered) {
    const ip = normalizeIpv4(String(row.address ?? ""))
    if (ip && !isPrivateIpv4(ip)) return ip
  }
  for (const row of ordered) {
    const ip = normalizeIpv4(String(row.address ?? ""))
    if (ip) return ip
  }

  throw new Error("Не удалось определить IP сервера для A-записи Cloudflare")
}

async function listDnsRecordsByName(token: string, zoneId: string, fqdn: string): Promise<CfDnsRecord[]> {
  return cloudflareRequest<CfDnsRecord[]>(
    token,
    `/zones/${zoneId}/dns_records?name=${encodeURIComponent(fqdn)}`,
  )
}

async function upsertARecord(token: string, zoneId: string, fqdn: string, ip: string): Promise<"updated" | "created" | "skipped_cname"> {
  const records = await listDnsRecordsByName(token, zoneId, fqdn)
  const existingA = records.find((record) => record.type === "A")
  if (existingA) {
    if (existingA.content === ip) return "updated"
    await cloudflareRequest<CfDnsRecord>(token, `/zones/${zoneId}/dns_records/${existingA.id}`, {
      method: "PATCH",
      body: JSON.stringify({
        type: "A",
        name: fqdn,
        content: ip,
        ttl: 120,
        proxied: false,
      }),
    })
    return "updated"
  }

  // CNAME на CN/SAN (алиас на канонический хост) — норма; A конфликтует с CNAME и для DNS-01 не нужен
  if (records.some((record) => record.type === "CNAME")) {
    return "skipped_cname"
  }

  await cloudflareRequest<{ id: string }>(token, `/zones/${zoneId}/dns_records`, {
    method: "POST",
    body: JSON.stringify({
      type: "A",
      name: fqdn,
      content: ip,
      ttl: 120,
      proxied: false,
    }),
  })
  return "created"
}

async function syncCertificateDomainRecords(
  token: string,
  domains: string[],
  serverIp: string,
  defaultZoneId?: string,
): Promise<{ skippedCname: string[] }> {
  const skippedCname: string[] = []
  for (const domain of domains) {
    const zoneId = await resolveZoneId(token, domain, defaultZoneId)
    const result = await upsertARecord(token, zoneId, domain, serverIp)
    if (result === "skipped_cname") skippedCname.push(domain)
  }
  return { skippedCname }
}

async function sleep(ms: number) {
  await new Promise((resolve) => setTimeout(resolve, ms))
}

async function getOrCreateAccountKey(): Promise<Buffer> {
  const existing = getAcmeAccountPrivateKey()
  if (existing) return Buffer.from(existing)
  const key = await acme.crypto.createPrivateKey()
  saveAcmeAccountPrivateKey(key.toString("utf8"))
  return key
}

async function ensureAcmeAccount(client: acme.Client): Promise<void> {
  try {
    client.getAccountUrl()
    return
  } catch {
    await client.createAccount({ termsOfServiceAgreed: true })
  }
}

function splitPemCertificates(pem: string): string[] {
  return [...pem.matchAll(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g)]
    .map((match) => match[0].trim())
    .filter(Boolean)
}

export async function issueCertificateWithCloudflareDns(params: {
  server: Server
  certName: string
  domainNames: string[]
  keyType: "rsa2048" | "ec256"
  trustStore: string[]
  onStep?: (step: string) => void
}): Promise<void> {
  const settings = getAcmeSettingsPublic()
  const token = getAcmeCloudflareToken()
  if (!token) throw new Error("Не настроен Cloudflare API token")

  const domains = [...new Set(params.domainNames.map((d) => d.trim().toLowerCase()).filter(Boolean))]
  if (domains.length === 0) throw new Error("Нужен хотя бы один домен")

  params.onStep?.("acme_order")
  const accountKey = await getOrCreateAccountKey()
  const client = new acme.Client({
    directoryUrl: settings.directoryUrl,
    accountKey,
  })
  await ensureAcmeAccount(client)

  const altNames = domains.slice(1)
  const privateKey = params.keyType === "ec256"
    ? await acme.crypto.createPrivateEcdsaKey("P-256")
    : await acme.crypto.createPrivateKey(2048)
  const [, csr] = await acme.crypto.createCsr({ commonName: domains[0], altNames }, privateKey)

  const order = await client.createOrder({
    identifiers: domains.map((value) => ({ type: "dns", value })),
  })

  const authorizations = await client.getAuthorizations(order)
  const txtCleanups: Array<{ zoneId: string; recordId: string }> = []

  try {
    params.onStep?.("dns_challenge")
    for (const authz of authorizations) {
      const challenge = authz.challenges.find((c) => c.type === "dns-01")
      if (!challenge) throw new Error(`Нет DNS-01 challenge для ${authz.identifier.value}`)
      const txtValue = await client.getChallengeKeyAuthorization(challenge)
      const zoneId = await resolveZoneId(token, authz.identifier.value, settings.defaultZoneId)
      const recordName = `_acme-challenge.${authz.identifier.value}`
      const recordId = await createTxtRecord(token, zoneId, recordName, txtValue)
      txtCleanups.push({ zoneId, recordId })
    }

    await sleep(15_000)

    for (const authz of authorizations) {
      const challenge = authz.challenges.find((c) => c.type === "dns-01")
      if (!challenge) continue
      await client.verifyChallenge(authz, challenge)
      await client.completeChallenge(challenge)
      await client.waitForValidStatus(authz)
    }

    params.onStep?.("finalize")
    const finalized = await client.finalizeOrder(order, csr)
    const certPem = await client.getCertificate(finalized)

    // A-sync опционален: DNS-01 уже завершён. CNAME на CN (msk2 → msk-gw02) не должен валить импорт.
    const clientRos = MikrotikClient.fromServer(params.server)
    try {
      params.onStep?.("dns_a_sync")
      const serverIp = await resolveServerPublicIp(params.server, clientRos)
      const { skippedCname } = await syncCertificateDomainRecords(
        token,
        domains,
        serverIp,
        settings.defaultZoneId,
      )
      if (skippedCname.length > 0) {
        params.onStep?.(
          `dns_a_sync_skip_cname:${skippedCname.join(",")}`,
        )
      }
    } catch (e) {
      const msg = e instanceof Error ? e.message : "ошибка DNS A-sync"
      params.onStep?.(`dns_a_sync_warn:${msg}`)
    }

    const trustStores = params.trustStore.filter(Boolean)
    const effectiveTrustStores = trustStores.length > 0 ? trustStores : ["www", "api"]
    const trustStoreCsv = effectiveTrustStores.join(",")

    params.onStep?.("import")
    const safeBase = params.certName.replace(/[^a-zA-Z0-9._-]+/g, "_")
    const chain = splitPemCertificates(certPem)
    const leafPem = chain[0] ?? certPem
    const chainRest = chain.slice(1)
    const certFile = `${safeBase}.crt`
    const keyFile = `${safeBase}.key`
    const certRouterFile = await clientRos.uploadTextFile(certFile, leafPem)
    const keyRouterFile = await clientRos.uploadTextFile(keyFile, privateKey.toString("utf8"))
    await clientRos.importCertificate({
      fileName: certRouterFile,
      name: params.certName,
      trusted: true,
      trustStore: trustStoreCsv,
    })
    await clientRos.importCertificate({
      fileName: keyRouterFile,
      name: params.certName,
      trusted: true,
      trustStore: trustStoreCsv,
    })

    for (let index = 0; index < chainRest.length; index += 1) {
      const isRoot = index === chainRest.length - 1
      const caName = isRoot ? `root_${params.certName}` : `root_${params.certName}_${index + 1}`
      const caFile = `${safeBase}_ca_${index + 1}.crt`
      const caRouterFile = await clientRos.uploadTextFile(caFile, chainRest[index]!)
      await clientRos.importCertificate({
        fileName: caRouterFile,
        name: caName,
        trusted: true,
        trustStore: trustStoreCsv,
      })
    }

    await clientRos.applyCertificateToServices(params.certName)
  } finally {
    params.onStep?.("cleanup")
    for (const item of txtCleanups) {
      try {
        await deleteTxtRecord(token, item.zoneId, item.recordId)
      } catch {
        /* ignore cleanup errors */
      }
    }
  }
}
