import type { RowDataPacket, ResultSetHeader } from 'mysql2'
import { db, DB } from './db'
import { creditDPoints, PD_PER_UNIT } from './dpoints'
import { getPaidService, fulfillOk, fulfillRefundEur } from './paid-services'

// Host de la API de SumUp, configurable por si hay que apuntar a otro entorno.
// Conviven dos versiones y NO son intercambiables: los checkouts van por v0.1 y
// los reembolsos por v1.0 (el `/v0.1/me/refund/{txn}` antiguo responde 409 a
// cualquier reembolso parcial). Ojo también con las unidades: v0.1 usa euros
// (1.30) y v1.0 céntimos (130).
const SUMUP_HOST = process.env.SUMUP_API_BASE || 'https://api.sumup.com'
const API = `${SUMUP_HOST}/v0.1`
const API_V1 = `${SUMUP_HOST}/v1.0`

/** SumUp está disponible sólo si hay API key y merchant code configurados. */
export function sumupConfigured(): boolean {
  return Boolean(process.env.SUMUP_API_KEY && process.env.SUMUP_MERCHANT_CODE)
}

function authHeaders(): Record<string, string> {
  return {
    Authorization: `Bearer ${process.env.SUMUP_API_KEY}`,
    'Content-Type': 'application/json',
  }
}

interface CreateParams {
  accountId: number
  username: string
  email: string
  acoreIp: string
  amount: number // importe en la moneda del comercio
  points: number
  reference: string
  description: string
  returnUrl: string
  // Para pagos de servicio (renombrar, etc.): al pagar se ejecuta la acción del
  // servicio sobre `characterName` en vez de acreditar PD. Sin `service` = compra de PD.
  service?: string
  characterName?: string
  // Campos extra del servicio (p.ej. { gold_amount }) que la entrega necesita.
  metadata?: Record<string, string>
}

/**
 * Crea un checkout hospedado de SumUp y registra el pago en `stripelog`
 * (reutilizamos la tabla de logs; `mode = 'sumup'`, `session_id = reference`).
 * Devuelve la URL del checkout hospedado para redirigir al usuario.
 */
export async function createSumUpCheckout(p: CreateParams): Promise<{ success: boolean; url?: string; error?: string }> {
  if (!sumupConfigured()) return { success: false, error: 'notConfigured' }
  try {
    const res = await fetch(`${API}/checkouts`, {
      method: 'POST',
      headers: authHeaders(),
      body: JSON.stringify({
        checkout_reference: p.reference,
        amount: p.amount,
        currency: process.env.SUMUP_CURRENCY || 'EUR',
        merchant_code: process.env.SUMUP_MERCHANT_CODE,
        description: p.description,
        return_url: p.returnUrl,
        hosted_checkout: { enabled: true },
      }),
    })
    if (!res.ok) return { success: false, error: `sumup_${res.status}` }
    const data: { id?: string; hosted_checkout_url?: string } = await res.json()
    const url = data.hosted_checkout_url
    if (!url) return { success: false, error: 'noHostedUrl' }

    await db(DB.default).query(
      'INSERT INTO stripelog (account_id, username, email, acore_ip, stripe_ip, product_name, amount, session_id, mode, character_name, service, metadata, timestamp, fulfilled) ' +
        'VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, NOW(), 0)',
      [
        p.accountId,
        p.username,
        p.email,
        p.acoreIp,
        p.description,
        p.amount,
        p.reference,
        'sumup',
        p.characterName ?? p.username,
        p.service ?? null,
        p.metadata ? JSON.stringify(p.metadata) : null,
      ],
    )
    return { success: true, url }
  } catch (e) {
    return { success: false, error: String(e) }
  }
}

/** ¿El checkout con esta referencia está PAGADO en SumUp? */
async function isReferencePaid(reference: string): Promise<boolean> {
  try {
    const res = await fetch(`${API}/checkouts?checkout_reference=${encodeURIComponent(reference)}`, {
      headers: authHeaders(),
    })
    if (!res.ok) return false
    const list: { status?: string }[] = await res.json()
    return Array.isArray(list) && list.some((c) => c.status === 'PAID')
  } catch {
    return false
  }
}

/**
 * Reembolsa un pago de SumUp, entero o en parte. `amountEur` omitido = todo.
 *
 * Tres cosas que hay que saber, todas comprobadas contra la API en sandbox:
 *  - No se reembolsa por referencia de checkout sino por TRANSACCIÓN, así que
 *    primero hay que sacar su id del checkout pagado.
 *  - El endpoint bueno es el de v1.0 (`API_V1`, ver arriba). El viejo
 *    `/v0.1/me/refund/{txn}` responde 409 a cualquier reembolso parcial.
 *  - ⚠️ El importe va en CÉNTIMOS y ENTERO (como Stripe, y al revés que el resto
 *    de la API v0.1, que usa unidades). Mandar 0.10 en vez de 10 NO da error: se
 *    trunca a 0, SumUp lo lee como "sin importe" y DEVUELVE EL PAGO ENTERO.
 *
 * Además hay un mínimo por reembolso (la API lo devuelve en `min_refundable_amount`;
 * son 20 céntimos en esta cuenta): por debajo responde 400 y no se puede devolver
 * a medias. Se registra para hacerlo a mano.
 */
export async function refundSumUpCheckout(reference: string, amountEur?: number): Promise<boolean> {
  if (!sumupConfigured() || !reference) return false
  if (amountEur !== undefined && !(amountEur > 0)) return false
  const merchant = process.env.SUMUP_MERCHANT_CODE ?? ''
  try {
    const res = await fetch(`${API}/checkouts?checkout_reference=${encodeURIComponent(reference)}`, {
      headers: authHeaders(),
    })
    if (!res.ok) return false
    const list: { status?: string; transaction_id?: string; transactions?: { id?: string }[] }[] = await res.json()
    const paid = Array.isArray(list) ? list.find((c) => c.status === 'PAID') : null
    const txn = paid?.transaction_id || paid?.transactions?.find((t) => t.id)?.id
    if (!txn) {
      console.error(`[sumup] ${reference}: pagado pero sin id de transacción; no se puede reembolsar`)
      return false
    }
    const cents = amountEur === undefined ? undefined : Math.round(amountEur * 100)
    const r = await fetch(
      `${API_V1}/merchants/${encodeURIComponent(merchant)}/payments/${encodeURIComponent(txn)}/refunds`,
      {
        method: 'POST',
        headers: authHeaders(),
        // Sin `amount` = reembolso total. Nunca mandar decimales: ver arriba.
        body: JSON.stringify(cents === undefined ? {} : { amount: cents }),
      },
    )
    if (!r.ok) {
      console.error(`[sumup] reembolso de ${reference} (${cents ?? 'total'} cts) rechazado: ${r.status} ${await r.text()}`)
      return false
    }
    return true
  } catch (e) {
    console.error(`[sumup] no se pudo reembolsar ${reference}:`, e)
    return false
  }
}

/**
 * Entrega un checkout SumUp pagado: verifica el estado, reclama de forma atómica
 * (fulfilled 0->1) y acredita los PD. Idempotente. Devuelve true si acreditó ahora.
 */
export async function fulfillSumUpCheckout(
  reference: string,
): Promise<{ ok: boolean; paid: boolean; service?: string; character?: string }> {
  if (!reference) return { ok: false, paid: false }
  const paid = await isReferencePaid(reference)
  if (!paid) return { ok: false, paid: false }

  const [rows] = await db(DB.default).query<RowDataPacket[]>(
    'SELECT id, account_id, amount, service, character_name, metadata FROM stripelog WHERE session_id = ? AND mode = ?',
    [reference, 'sumup'],
  )
  const log = rows[0]
  if (!log) return { ok: false, paid: true }

  // Reclamo atómico: sólo gana quien pasa fulfilled 0->1.
  const [res] = await db(DB.default).query<ResultSetHeader>(
    'UPDATE stripelog SET fulfilled = 1 WHERE id = ? AND fulfilled = 0',
    [log.id],
  )
  if (res.affectedRows === 0) return { ok: false, paid: true }

  // Pago de servicio (renombrar, oro…): ejecuta la acción sobre el personaje.
  const service = log.service ? String(log.service) : ''
  const cfg = service ? getPaidService(service) : null
  if (cfg) {
    let meta: Record<string, string> = { service }
    try {
      if (log.metadata) meta = { ...JSON.parse(String(log.metadata)), service }
    } catch {
      /* metadata corrupto: seguimos con lo básico */
    }
    const outcome = await cfg.fulfill(String(log.character_name), meta)
    // Entrega parcial: el pago ya está cobrado y no hay reintento (se reclama una
    // sola vez), así que se devuelve por SumUp lo que no se pudo entregar.
    const refundEur = fulfillRefundEur(outcome)
    if (refundEur > 0 && !(await refundSumUpCheckout(reference, refundEur))) {
      console.error(
        `[sumup] ${service}: entrega parcial y el reembolso de ${refundEur} € FALLÓ en ${reference}.` +
          ' Hay que devolverlo a mano desde el panel de SumUp.',
      )
    }
    return { ok: fulfillOk(outcome), paid: true, service, character: String(log.character_name) }
  }

  // Sin servicio: compra de PD (comportamiento por defecto).
  const points = Math.round(Number(log.amount) * PD_PER_UNIT)
  const ok = await creditDPoints(Number(log.account_id), points)
  return { ok, paid: true }
}

/**
 * Reconciliación: SumUp ya no ofrece webhooks, así que la entrega no puede
 * depender de que el navegador vuelva al `return_url`. Esta función recorre los
 * checkouts SumUp pendientes (fulfilled=0) registrados en `stripelog`,
 * consulta su estado en la API de SumUp y acredita los que estén PAID.
 *
 * - Sin `accountId`: backstop global (para un cron). Con `accountId`: reconcilia
 *   sólo los pendientes de esa cuenta (al cargar /account o /d-points).
 * - Acota por antigüedad: los no pagados acaban EXPIRED en SumUp, no tiene
 *   sentido reconsultarlos indefinidamente.
 */
export async function reconcileSumUpCheckouts(opts?: { accountId?: number; maxAgeDays?: number }): Promise<number> {
  if (!sumupConfigured()) return 0
  const maxAgeDays = opts?.maxAgeDays ?? 7
  let sql =
    "SELECT session_id FROM stripelog WHERE mode = 'sumup' AND fulfilled = 0 AND timestamp > (NOW() - INTERVAL ? DAY)"
  const params: (string | number)[] = [maxAgeDays]
  if (opts?.accountId) {
    sql += ' AND account_id = ?'
    params.push(opts.accountId)
  }
  sql += ' ORDER BY id DESC LIMIT 50'

  let rows: RowDataPacket[]
  try {
    ;[rows] = await db(DB.default).query<RowDataPacket[]>(sql, params)
  } catch {
    return 0
  }

  let credited = 0
  for (const r of rows) {
    try {
      const res = await fulfillSumUpCheckout(String(r.session_id))
      if (res.ok) credited++
    } catch {
      /* seguir con el resto */
    }
  }
  return credited
}
