import { randomUUID } from "node:crypto"
import { desc, eq } from "drizzle-orm"
import type {
  AppUserCreate,
  AppUserRead,
  AppUserUpdate,
  CatalogInterface,
  InterfaceType,
  SectionPerm,
  ServerPerm,
  UserBinding,
  UserBindingCreate,
} from "@mmapp/contracts/users"
import { db } from "../../../db/index.js"
import { servers, trafficSamples } from "../../../db/schema.js"
import {
  createBindingRow,
  createUserRow,
  deleteBindingRowById,
  deleteUserRowById,
  getBindingByServerIfacePeer,
  getBindingRowById,
  getUserRowById,
  getUserRowByLogin,
  listBindingRows,
  listBindingRowsByUser,
  listUserRows,
  updateUserRowById,
  type AppUserRow,
  type BindingRow,
} from "../repository/users-repository.js"
import { getLatestSnapshot } from "../../servers/repository/servers-repository.js"
import {
  isUniqueConstraintError,
  mapRosInterfaceType,
  parseRawInterfaces,
} from "../iface-type.js"
import {
  normalizeBindingPeer,
  PeerBindError,
  peerDisplayName,
} from "../peer-bind.js"
import { listWireGuardPeersForCatalog } from "../../../services/wireguard-live.js"

export class UsersServiceError extends Error {
  constructor(
    message: string,
    public readonly status: number,
  ) {
    super(message)
    this.name = "UsersServiceError"
  }
}

function parseJsonArray<T>(raw: string, fallback: T[]): T[] {
  try {
    const parsed = JSON.parse(raw) as unknown
    return Array.isArray(parsed) ? (parsed as T[]) : fallback
  } catch {
    return fallback
  }
}

function initials(name: string): string {
  const parts = name.trim().split(/\s+/).filter(Boolean)
  return parts.map((p) => p[0] ?? "").slice(0, 2).join("").toUpperCase() || "??"
}

function serverMeta(serverId: number): { name: string; site: string; country: string } {
  const row = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
  return {
    name: row?.name || row?.host || String(serverId),
    site: row?.site || "—",
    country: row?.country || "UN",
  }
}

function toBindingDto(row: BindingRow): UserBinding {
  const meta = serverMeta(row.serverId)
  return {
    id: row.id,
    userId: row.userId,
    serverId: row.serverId,
    serverName: meta.name,
    serverSite: meta.site,
    serverCountry: meta.country,
    interfaceName: row.interfaceName,
    interfaceType: row.interfaceType,
    peerPublicKey: row.peerPublicKey ?? "",
    peerName: row.peerName ?? "",
    comment: row.comment,
    createdAt: row.createdAt,
    updatedAt: row.updatedAt,
  }
}

function toUserDto(row: AppUserRow, bindings: BindingRow[]): AppUserRead {
  return {
    id: row.id,
    name: row.name,
    login: row.login,
    email: row.email,
    role: row.role,
    active: Boolean(row.active),
    avatar: row.avatar,
    lastSeen: row.lastSeen ?? null,
    sections: parseJsonArray<SectionPerm>(row.sectionsJson, []),
    servers: parseJsonArray<ServerPerm>(row.serversJson, []),
    bindings: bindings.map(toBindingDto),
    createdAt: row.createdAt,
    updatedAt: row.updatedAt,
  }
}

export function listUsers(): AppUserRead[] {
  const users = listUserRows()
  const allBindings = listBindingRows()
  const byUser = new Map<string, BindingRow[]>()
  for (const b of allBindings) {
    const arr = byUser.get(b.userId) ?? []
    arr.push(b)
    byUser.set(b.userId, arr)
  }
  return users.map((u) => toUserDto(u, byUser.get(u.id) ?? []))
}

export function getUserById(id: string): AppUserRead | undefined {
  const row = getUserRowById(id)
  if (!row) return undefined
  return toUserDto(row, listBindingRowsByUser(id))
}

export function createUser(input: AppUserCreate): AppUserRead {
  const login = input.login.trim()
  if (getUserRowByLogin(login)) {
    throw new UsersServiceError("Логин уже занят", 409)
  }
  const now = new Date().toISOString()
  const row = createUserRow({
    id: randomUUID(),
    name: input.name.trim(),
    login,
    email: input.email.trim(),
    role: input.role ?? "viewer",
    active: input.active ?? true,
    avatar: (input.avatar ?? "").trim() || initials(input.name),
    lastSeen: input.lastSeen ?? null,
    sectionsJson: JSON.stringify(input.sections ?? []),
    serversJson: JSON.stringify(input.servers ?? []),
    createdAt: now,
    updatedAt: now,
  })
  return toUserDto(row, [])
}

export function updateUser(id: string, input: AppUserUpdate): AppUserRead {
  const existing = getUserRowById(id)
  if (!existing) throw new UsersServiceError("Пользователь не найден", 404)
  if (input.login != null) {
    const other = getUserRowByLogin(input.login.trim())
    if (other && other.id !== id) throw new UsersServiceError("Логин уже занят", 409)
  }
  const patch: Partial<AppUserRow> = { updatedAt: new Date().toISOString() }
  if (input.name != null) patch.name = input.name.trim()
  if (input.login != null) patch.login = input.login.trim()
  if (input.email != null) patch.email = input.email.trim()
  if (input.role != null) patch.role = input.role
  if (input.active != null) patch.active = input.active
  if (input.avatar != null) patch.avatar = input.avatar.trim() || existing.avatar
  if (input.lastSeen !== undefined) patch.lastSeen = input.lastSeen
  if (input.sections != null) patch.sectionsJson = JSON.stringify(input.sections)
  if (input.servers != null) patch.serversJson = JSON.stringify(input.servers)
  const updated = updateUserRowById(id, patch)
  return toUserDto(updated, listBindingRowsByUser(id))
}

export function deleteUser(id: string): void {
  const existing = getUserRowById(id)
  if (!existing) throw new UsersServiceError("Пользователь не найден", 404)
  deleteUserRowById(id)
}

export function addBinding(userId: string, input: UserBindingCreate): UserBinding {
  const user = getUserRowById(userId)
  if (!user) throw new UsersServiceError("Пользователь не найден", 404)
  const server = db.select().from(servers).where(eq(servers.id, input.serverId)).limit(1).all()[0]
  if (!server) throw new UsersServiceError("Сервер не найден", 404)
  const ifaceName = input.interfaceName.trim()
  if (!ifaceName) throw new UsersServiceError("Имя интерфейса обязательно", 400)
  const type: InterfaceType = input.interfaceType ?? inferIfaceType(input.serverId, ifaceName)
  let peerPublicKey = ""
  try {
    peerPublicKey = normalizeBindingPeer(type, input.peerPublicKey)
  } catch (err) {
    if (err instanceof PeerBindError) throw new UsersServiceError(err.message, err.status)
    throw err
  }
  const peerName = type === "wg"
    ? peerDisplayName({
      publicKey: peerPublicKey,
      name: input.peerName,
    })
    : ""
  const taken = getBindingByServerIfacePeer(input.serverId, ifaceName, peerPublicKey)
  if (taken) {
    throw new UsersServiceError(
      type === "wg"
        ? "Этот пир уже привязан к другому пользователю"
        : "Интерфейс уже привязан к другому пользователю",
      409,
    )
  }
  const now = new Date().toISOString()
  try {
    const row = createBindingRow({
      id: randomUUID(),
      userId,
      serverId: input.serverId,
      interfaceName: ifaceName,
      interfaceType: type,
      peerPublicKey,
      peerName,
      comment: (input.comment ?? "").trim(),
      createdAt: now,
      updatedAt: now,
    })
    return toBindingDto(row)
  } catch (err) {
    if (isUniqueConstraintError(err)) {
      throw new UsersServiceError(
        type === "wg"
          ? "Этот пир уже привязан к другому пользователю"
          : "Интерфейс уже привязан к другому пользователю",
        409,
      )
    }
    throw err
  }
}

export function removeBinding(userId: string, bindingId: string): void {
  const row = getBindingRowById(bindingId)
  if (!row || row.userId !== userId) {
    throw new UsersServiceError("Привязка не найдена", 404)
  }
  deleteBindingRowById(bindingId)
}

function inferIfaceType(serverId: number, ifaceName: string): InterfaceType {
  const snap = getLatestSnapshot(serverId)
  const parsed = parseRawInterfaces(snap?.rawInterfaces)
  const found = parsed.find((i) => i.name === ifaceName)
  return found?.type ?? "other"
}

export async function listInterfaceCatalog(serverId: number): Promise<CatalogInterface[]> {
  const server = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
  if (!server) throw new UsersServiceError("Сервер не найден", 404)

  const snap = getLatestSnapshot(serverId)
  let ifaces = parseRawInterfaces(snap?.rawInterfaces)
  if (ifaces.length === 0) {
    const last = db
      .select({ sampledAt: trafficSamples.sampledAt })
      .from(trafficSamples)
      .where(eq(trafficSamples.serverId, serverId))
      .orderBy(desc(trafficSamples.sampledAt))
      .limit(1)
      .all()[0]
    if (last) {
      const rows = db
        .select({
          interfaceName: trafficSamples.interfaceName,
          peerPublicKey: trafficSamples.peerPublicKey,
          running: trafficSamples.running,
          disabled: trafficSamples.disabled,
        })
        .from(trafficSamples)
        .where(eq(trafficSamples.serverId, serverId))
        .all()
        .filter((r) => r.interfaceName && !/^(lo|loopback)/i.test(r.interfaceName) && !(r.peerPublicKey ?? ""))
      const seen = new Set<string>()
      ifaces = []
      for (const r of rows) {
        if (seen.has(r.interfaceName)) continue
        seen.add(r.interfaceName)
        ifaces.push({
          name: r.interfaceName,
          type: mapRosInterfaceType("", r.interfaceName),
          running: Boolean(r.running),
          disabled: Boolean(r.disabled),
        })
      }
    }
  }

  const bindings = listBindingRows().filter((b) => b.serverId === serverId)
  const usersById = new Map(listUserRows().map((u) => [u.id, u]))
  const hasWg = ifaces.some((i) => i.type === "wg")
  const wgLive = hasWg
    ? await listWireGuardPeersForCatalog(serverId)
    : { peers: [] as Awaited<ReturnType<typeof listWireGuardPeersForCatalog>>["peers"] }
  const peersByIface = new Map<string, typeof wgLive.peers>()
  for (const peer of wgLive.peers) {
    const list = peersByIface.get(peer.interfaceName) ?? []
    list.push(peer)
    peersByIface.set(peer.interfaceName, list)
  }

  return ifaces.map((iface) => {
    const ifaceBind = bindings.find((b) => b.interfaceName === iface.name && !(b.peerPublicKey ?? ""))
    const owner = ifaceBind ? usersById.get(ifaceBind.userId) : undefined
    const base: CatalogInterface = {
      name: iface.name,
      type: iface.type,
      running: iface.running,
      disabled: iface.disabled,
      boundUserId: ifaceBind?.userId ?? null,
      boundUserLogin: owner?.login ?? null,
    }
    if (iface.type !== "wg") return base
    const livePeers = peersByIface.get(iface.name) ?? []
    return {
      ...base,
      peersError: wgLive.error,
      peers: livePeers.map((p) => {
        const bind = bindings.find((b) => b.interfaceName === iface.name && b.peerPublicKey === p.publicKey)
        const peerOwner = bind ? usersById.get(bind.userId) : undefined
        return {
          publicKey: p.publicKey,
          name: peerDisplayName({ publicKey: p.publicKey, name: p.name, comment: p.comment }),
          comment: p.comment,
          allowedIps: p.allowedIps,
          latestHandshake: p.latestHandshake,
          boundUserId: bind?.userId ?? null,
          boundUserLogin: peerOwner?.login ?? null,
        }
      }),
    }
  }).sort((a, b) => a.name.localeCompare(b.name))
}

export { parseRawInterfaces, mapRosInterfaceType }
