import crypto from 'node:crypto'
import type { RowDataPacket, ResultSetHeader } from 'mysql2'
import { db, DB } from './db'
import { authenticate } from './auth'
import { checkSecurityToken } from './security-token'
import { transferDPoints } from './dpoints'
import { executeSoapCommand } from './soap'

/**
 * Comercio de PD: códigos de 1 solo uso que valen X PD a cambio de X oro.
 *
 * - El vendedor crea un código (VENTA) indicando PD, oro y el personaje de SU
 *   cuenta que recibirá el oro por correo cuando se canjee.
 * - El comprador canjea el código (COMPRA) indicando de qué personaje de SU
 *   cuenta se cobra el oro.
 *
 * Al canjear (decisión de diseño «al canjear», no escrow):
 *   1. Se descuenta el oro del personaje del comprador (debe estar desconectado).
 *   2. Los PD pasan de la cuenta del creador a la del comprador.
 *   3. El oro se envía por correo (SOAP) al personaje del creador.
 * Cada código dura 1 hora. Tras un canje correcto, la cuenta del comprador queda
 * «bloqueada» 5 s (no puede canjear otro código) como medida de seguridad.
 */

const CODE_TTL_MS = 60 * 60 * 1000 // 1 hora
const REDEEM_LOCK_SECONDS = 5
const GOLD_TO_COPPER = 10000
const MAX_AMOUNT = 99999 // el input admite máx. 5 dígitos
/** Nombres de personaje válidos (evita inyección en comandos SOAP). */
const NAME_RE = /^[A-Za-z]{1,12}$/

export interface Result {
  success: boolean
  error?: string
}

export interface TradeCodeInfo {
  code: string
  points: number
  gold: number
  expiresAt: string
}

function validAmount(n: number): boolean {
  return Number.isInteger(n) && n >= 1 && n <= MAX_AMOUNT
}

/** Genera un código único legible de <=25 caracteres (p.ej. TP-3F9K2A7QX8B1). */
function generateCode(): string {
  return 'TP-' + crypto.randomBytes(9).toString('hex').toUpperCase() // "TP-" + 18 hex = 21
}

/** Marca como expirados los códigos activos cuyo plazo venció (limpieza perezosa). */
async function expireOldCodes(): Promise<void> {
  try {
    await db(DB.default).query(
      "UPDATE tradecode SET status = 'expired' WHERE status = 'active' AND expires_at <= NOW()",
    )
  } catch {
    /* ignore */
  }
}

/** True si el personaje pertenece a la cuenta. Devuelve el saldo (copper) si sí. */
async function characterMoney(accountId: number, name: string): Promise<{ money: number; online: boolean } | null> {
  const [rows] = await db(DB.characters).query<RowDataPacket[]>(
    'SELECT money, online FROM characters WHERE name = ? AND account = ? LIMIT 1',
    [name, accountId],
  )
  if (!rows[0]) return null
  return { money: Number(rows[0].money), online: rows[0].online === 1 }
}

/* -------------------------------- VENTA ----------------------------------- */
export async function createTradeCode(params: {
  creatorAccount: number
  creatorEmail: string
  creatorCharacter: string
  points: number
  gold: number
  password: string
  token: string
  turnstileOk: boolean
}): Promise<{ success: boolean; error?: string; code?: TradeCodeInfo }> {
  const { creatorAccount, creatorEmail, creatorCharacter, points, gold, password, token, turnstileOk } = params

  if (!creatorCharacter || !password || !token) return { success: false, error: 'missingFields' }
  if (!validAmount(points) || !validAmount(gold)) return { success: false, error: 'invalidAmount' }
  if (!NAME_RE.test(creatorCharacter)) return { success: false, error: 'characterNotFound' }
  if (!turnstileOk) return { success: false, error: 'captcha' }

  // El personaje que recibirá el oro debe ser de la cuenta del creador.
  const owned = await characterMoney(creatorAccount, creatorCharacter)
  if (!owned) return { success: false, error: 'characterNotOwned' }

  if (!(await authenticate(creatorEmail, password))) return { success: false, error: 'wrongPassword' }
  if (!(await checkSecurityToken(creatorAccount, token))) return { success: false, error: 'invalidToken' }

  await expireOldCodes()

  // No hay escrow: comprobamos que el saldo cubre TODOS los códigos activos + este,
  // para no poder crear códigos que no se puedan respaldar.
  try {
    const [bal] = await db(DB.default).query<RowDataPacket[]>(
      'SELECT dp FROM api_points WHERE accountID = ?',
      [creatorAccount],
    )
    const balance = bal[0] ? Number(bal[0].dp) : 0
    const [act] = await db(DB.default).query<RowDataPacket[]>(
      "SELECT COALESCE(SUM(points), 0) AS reserved FROM tradecode WHERE creator_account = ? AND status = 'active' AND expires_at > NOW()",
      [creatorAccount],
    )
    const reserved = Number(act[0]?.reserved ?? 0)
    if (reserved + points > balance) return { success: false, error: 'insufficientPoints' }
  } catch {
    return { success: false, error: 'genericError' }
  }

  const code = generateCode()
  const expiresAt = new Date(Date.now() + CODE_TTL_MS)
  try {
    await db(DB.default).query(
      `INSERT INTO tradecode
        (code, creator_account, creator_character, points, gold, status, created_at, expires_at)
       VALUES (?, ?, ?, ?, ?, 'active', NOW(), ?)`,
      [code, creatorAccount, creatorCharacter, points, gold, expiresAt],
    )
  } catch {
    return { success: false, error: 'genericError' }
  }
  return { success: true, code: { code, points, gold, expiresAt: expiresAt.toISOString() } }
}

/* ------------------------------- CHEQUEAR --------------------------------- */
export async function checkTradeCode(code: string): Promise<{ success: boolean; error?: string; points?: number; gold?: number; expiresAt?: string }> {
  const trimmed = code.trim()
  if (!trimmed) return { success: false, error: 'missingFields' }
  await expireOldCodes()
  try {
    const [rows] = await db(DB.default).query<RowDataPacket[]>(
      "SELECT points, gold, status, expires_at FROM tradecode WHERE code = ? LIMIT 1",
      [trimmed],
    )
    const row = rows[0]
    if (!row) return { success: false, error: 'codeNotFound' }
    if (row.status !== 'active' || new Date(row.expires_at).getTime() <= Date.now()) {
      return { success: false, error: 'codeUnavailable' }
    }
    return { success: true, points: Number(row.points), gold: Number(row.gold), expiresAt: new Date(row.expires_at).toISOString() }
  } catch {
    return { success: false, error: 'genericError' }
  }
}

/* -------------------------------- CANJEAR --------------------------------- */
export async function redeemTradeCode(params: {
  buyerAccount: number
  buyerEmail: string
  buyerCharacter: string
  code: string
  password: string
  token: string
  turnstileOk: boolean
}): Promise<{ success: boolean; error?: string; points?: number; gold?: number }> {
  const { buyerAccount, buyerEmail, buyerCharacter, code, password, token, turnstileOk } = params
  const trimmedCode = code.trim()

  if (!trimmedCode || !buyerCharacter || !password || !token) return { success: false, error: 'missingFields' }
  if (!NAME_RE.test(buyerCharacter)) return { success: false, error: 'characterNotOwned' }
  if (!turnstileOk) return { success: false, error: 'captcha' }

  if (!(await authenticate(buyerEmail, password))) return { success: false, error: 'wrongPassword' }
  if (!(await checkSecurityToken(buyerAccount, token))) return { success: false, error: 'invalidToken' }

  // Bloqueo de 5 s: la cuenta no puede canjear si acaba de canjear otro código.
  try {
    const [recent] = await db(DB.default).query<RowDataPacket[]>(
      `SELECT id FROM tradecode
        WHERE redeemer_account = ? AND redeemed_at > (NOW() - INTERVAL ? SECOND) LIMIT 1`,
      [buyerAccount, REDEEM_LOCK_SECONDS],
    )
    if (recent[0]) return { success: false, error: 'locked' }
  } catch {
    /* ignore */
  }

  await expireOldCodes()

  // Carga y valida el código.
  let creatorAccount = 0
  let creatorCharacter = ''
  let points = 0
  let gold = 0
  try {
    const [rows] = await db(DB.default).query<RowDataPacket[]>(
      "SELECT creator_account, creator_character, points, gold, status, expires_at FROM tradecode WHERE code = ? LIMIT 1",
      [trimmedCode],
    )
    const row = rows[0]
    if (!row) return { success: false, error: 'codeNotFound' }
    if (row.status !== 'active' || new Date(row.expires_at).getTime() <= Date.now()) {
      return { success: false, error: 'codeUnavailable' }
    }
    creatorAccount = Number(row.creator_account)
    creatorCharacter = String(row.creator_character)
    points = Number(row.points)
    gold = Number(row.gold)
  } catch {
    return { success: false, error: 'genericError' }
  }

  if (creatorAccount === buyerAccount) return { success: false, error: 'cannotRedeemOwn' }
  if (!NAME_RE.test(creatorCharacter)) return { success: false, error: 'genericError' }

  // El personaje del comprador debe ser suyo, estar OFFLINE y tener oro suficiente.
  const buyerChar = await characterMoney(buyerAccount, buyerCharacter)
  if (!buyerChar) return { success: false, error: 'characterNotOwned' }
  if (buyerChar.online) return { success: false, error: 'characterOnline' }
  const costCopper = gold * GOLD_TO_COPPER
  if (buyerChar.money < costCopper) return { success: false, error: 'insufficientGold' }

  // 1) Reclama el código de forma atómica (evita doble canje concurrente).
  let claimed = false
  try {
    const [res] = await db(DB.default).query<ResultSetHeader>(
      `UPDATE tradecode
         SET status = 'redeemed', redeemer_account = ?, redeemer_character = ?, redeemed_at = NOW()
       WHERE code = ? AND status = 'active' AND expires_at > NOW()`,
      [buyerAccount, buyerCharacter, trimmedCode],
    )
    claimed = res.affectedRows === 1
  } catch {
    return { success: false, error: 'genericError' }
  }
  if (!claimed) return { success: false, error: 'codeUnavailable' }

  const revertClaim = async () => {
    try {
      await db(DB.default).query(
        "UPDATE tradecode SET status = 'active', redeemer_account = NULL, redeemer_character = NULL, redeemed_at = NULL WHERE code = ?",
        [trimmedCode],
      )
    } catch {
      /* ignore */
    }
  }

  // 2) Cobra el oro al personaje del comprador (condicional: offline y saldo).
  let goldTaken = false
  try {
    const [res] = await db(DB.characters).query<ResultSetHeader>(
      'UPDATE characters SET money = money - ? WHERE name = ? AND account = ? AND online = 0 AND money >= ?',
      [costCopper, buyerCharacter, buyerAccount, costCopper],
    )
    goldTaken = res.affectedRows === 1
  } catch {
    goldTaken = false
  }
  if (!goldTaken) {
    await revertClaim()
    return { success: false, error: 'insufficientGold' }
  }

  const refundGold = async () => {
    try {
      await db(DB.characters).query('UPDATE characters SET money = money + ? WHERE name = ? AND account = ?', [
        costCopper,
        buyerCharacter,
        buyerAccount,
      ])
    } catch {
      /* ignore */
    }
  }

  // 3) Mueve los PD del creador al comprador (transacción con bloqueo de fila).
  const pdMove = await transferDPoints(creatorAccount, buyerAccount, points)
  if (!pdMove.success) {
    await refundGold()
    await revertClaim()
    return { success: false, error: pdMove.error === 'insufficientFunds' ? 'creatorInsufficient' : 'genericError' }
  }

  // 4) Envía el oro por correo al personaje del creador (SOAP). Último paso.
  const subject = 'Comercio de PD'
  const body = `Has recibido ${gold} de oro por el canje de tu codigo ${trimmedCode}.`
  const soap = await executeSoapCommand(`.send money ${creatorCharacter} '${subject}' '${body}' ${costCopper}`)
  if (soap === null) {
    // Compensa: revierte PD y oro, y reabre el código.
    await transferDPoints(buyerAccount, creatorAccount, points)
    await refundGold()
    await revertClaim()
    return { success: false, error: 'deliveryFailed' }
  }

  return { success: true, points, gold }
}
