import { BadRequestException } from '@nestjs/common';

/**
 * Paraguay no usa horario de verano desde 2024: offset fijo todo el año.
 */
const OFFSET_ASUNCION_HORAS = 3;

/**
 * "Ahora", pero como un Date cuyos dígitos serán los correctos de Asunción
 * al grabarse en una columna `timestamp` (sin zona horaria) vía Prisma.
 *
 * Prisma fija la sesión de Postgres a UTC en cada conexión, así que un
 * `new Date()` normal se graba con los dígitos de reloj UTC en una columna
 * `timestamp` — 3 horas adelantado respecto a la hora real de Paraguay
 * (confirmado: factura recién emitida a las 16:41 real quedó grabada como
 * 19:41). Restar el offset ANTES de que Prisma la convierta a UTC hace que
 * los dígitos que terminan en la base sean los correctos.
 *
 * Parche temporal (Fix B) para los campos fiscales que se llenan con la
 * hora de emisión (`dfeemide` en facturas, notas de crédito, remisiones).
 * El fix de fondo es migrar esas columnas a `timestamptz`, donde este
 * ajuste deja de ser necesario.
 */
export function nowAsuncionNaive(): Date {
  return new Date(Date.now() - OFFSET_ASUNCION_HORAS * 60 * 60 * 1000);
}

/**
 * Convierte string a Date para guardar en Prisma/BD
 * String → Date (para INSERT/UPDATE)
 */
export function toPrismaDate(input?: string | Date | null): Date | null {
  if (!input) return null;
  if (input instanceof Date) return input;
  // normaliza "YYYY-MM-DD HH:mm:ss" -> "YYYY-MM-DDTHH:mm:ss"
  const withT = input.includes('T') ? input : input.replace(' ', 'T');
  // si no trae zona horaria, asume UTC; agrega 'Z' al final
  const iso = /[zZ]|[+-]\d{2}:\d{2}$/.test(withT) ? withT : `${withT}Z`;
  const d = new Date(iso);
  if (isNaN(d.getTime())) {
    // último recurso: aceptar solo fecha "YYYY-MM-DD"
    const onlyDate = /^\d{4}-\d{2}-\d{2}$/.test(input) ? new Date(`${input}T00:00:00Z`) : null;
    if (!onlyDate) throw new BadRequestException('Formato de fecha inválido, use ISO-8601');
    return onlyDate;
  }
  return d;
}

/**
 * Convierte Date a string para responder al frontend.
 *
 * **Estándar del proyecto** (ver `pos-ventas/docs/ui-standards.md > Fechas`):
 * - `dateOnly = true`  → para **fechas-calendario** (fecha_emision, fecha_vencimiento,
 *   fecha_factura, fecha_documento, etc. — días elegidos por el usuario).
 *   Retorna `"YYYY-MM-DD"`. El frontend lo formatea con `fmtFechaCalendario` (sin TZ shift).
 * - `dateOnly = false` → para **timestamps absolutos** (created_at, updated_at,
 *   fecha_registro, fecha_anulacion, fecha_envio_sifen, etc. — instantes del sistema).
 *   Retorna ISO completo con `Z`. El frontend lo formatea con `fmtFechaHora` /
 *   `fmtFechaHoraSoloFecha` (convierte a `America/Asuncion`).
 *
 * Regla práctica: si NO te importa la hora, es calendario (`true`). Si te importa
 * cuándo exactamente pasó algo en el servidor, es timestamp (`false`).
 *
 * @param date - Fecha a convertir
 * @param dateOnly - Ver descripción arriba
 */
export function formatDateResponse(date: Date | null | undefined, dateOnly = false): string | null {
  if (!date) return null;
  try {
    const isoString = date.toISOString();
    return dateOnly ? isoString.split('T')[0] : isoString;
  } catch {
    return null;
  }
}

/**
 * Formatea fecha con hora en formato YYYY-MM-DD HH:MM:SS
 */
export function formatDateTimeResponse(date: Date | null | undefined): string | null {
  if (!date) return null;
  try {
    const d = new Date(date);
    const year = d.getFullYear();
    const month = String(d.getMonth() + 1).padStart(2, '0');
    const day = String(d.getDate()).padStart(2, '0');
    const hours = String(d.getHours()).padStart(2, '0');
    const minutes = String(d.getMinutes()).padStart(2, '0');
    const seconds = String(d.getSeconds()).padStart(2, '0');
    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  } catch {
    return null;
  }
}

// ── Helpers de timezone Paraguay (America/Asuncion) ───────────────────────
//
// Los filtros por "día" sobre columnas `Timestamptz` deben usar los límites del
// día calendario en la TZ del usuario (Asunción), no en UTC ni en la TZ del
// servidor. Estas funciones convierten un `YYYY-MM-DD` (día calendario local)
// al instante UTC exacto que corresponde a 00:00:00 y 23:59:59.999 en Asunción.
//
// Ejemplo: startOfDayAsuncion('2026-07-16') → 2026-07-16T03:00:00.000Z (UTC-3)
// Ejemplo: endOfDayAsuncion('2026-07-16')   → 2026-07-17T02:59:59.999Z

const ASUNCION_TZ = 'America/Asuncion';

/** Offset (min) de la TZ Asunción para un instante UTC dado. Positivo si al este de UTC. */
function getAsuncionOffsetMinutes(atUtc: Date): number {
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone: ASUNCION_TZ,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false,
  }).formatToParts(atUtc);
  const map: Record<string, string> = {};
  for (const p of parts) map[p.type] = p.value;
  const hour = map.hour === '24' ? '00' : map.hour;
  const asIfUtc = Date.UTC(
    Number(map.year),
    Number(map.month) - 1,
    Number(map.day),
    Number(hour),
    Number(map.minute),
    Number(map.second),
  );
  return (asIfUtc - atUtc.getTime()) / 60000;
}

/** Convierte wall-time Asunción (YYYY-MM-DD + h:m:s.ms) al instante UTC correspondiente. */
function asuncionWallTimeToUtc(
  ymd: string,
  h = 0,
  m = 0,
  s = 0,
  ms = 0,
): Date {
  const y = Number(ymd.slice(0, 4));
  const mo = Number(ymd.slice(5, 7)) - 1;
  const d = Number(ymd.slice(8, 10));
  // Primer intento tratando la wall-time como si fuera UTC.
  const guess = new Date(Date.UTC(y, mo, d, h, m, s, ms));
  // Corrección por el offset real de Asunción en ese momento (maneja DST si aplica).
  const offsetMin = getAsuncionOffsetMinutes(guess);
  return new Date(guess.getTime() - offsetMin * 60000);
}

/** Instante UTC correspondiente a 00:00:00.000 del día `ymd` (YYYY-MM-DD) en Asunción. */
export function startOfDayAsuncion(ymd: string): Date {
  return asuncionWallTimeToUtc(ymd, 0, 0, 0, 0);
}

/** Instante UTC correspondiente a 23:59:59.999 del día `ymd` (YYYY-MM-DD) en Asunción. */
export function endOfDayAsuncion(ymd: string): Date {
  return asuncionWallTimeToUtc(ymd, 23, 59, 59, 999);
}

/** Día calendario en Asunción (YYYY-MM-DD) para un instante UTC. */
export function ymdInAsuncion(atUtc: Date = new Date()): string {
  const parts = new Intl.DateTimeFormat('en-CA', {
    timeZone: ASUNCION_TZ,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).format(atUtc);
  return parts; // en-CA ya devuelve YYYY-MM-DD
}

// ── Guards de fecha para filtros de queries ──────────────────────────────────
// Evitan que un input malformado (string parcial, año fuera de rango, basura)
// llegue a Prisma como `Invalid Date` o un año absurdo (092020 / 275760) y tire
// PrismaClientValidationError / "Could not convert argument value ... to ArgumentValue".
const ANIO_MIN_QUERY = 1900;
const ANIO_MAX_QUERY = 9999; // límite seguro para Postgres/Prisma

/** Parsea un string arbitrario a Date válida y en rango; `undefined` si no lo es. */
export function parseFechaValida(input?: string | null): Date | undefined {
  if (!input) return undefined;
  const d = new Date(input);
  if (isNaN(d.getTime())) return undefined;
  const y = d.getFullYear();
  if (y < ANIO_MIN_QUERY || y > ANIO_MAX_QUERY) return undefined;
  return d;
}

/** Parsea 'YYYY-MM-DD' a Date local (inicio o fin del día); `undefined` si es inválida. */
export function parseYmdValida(input?: string | null, finDelDia = false): Date | undefined {
  if (!input) return undefined;
  const partes = input.split('-').map(Number);
  if (partes.length !== 3 || partes.some((n) => Number.isNaN(n))) return undefined;
  const [y, m, d] = partes;
  if (y < ANIO_MIN_QUERY || y > ANIO_MAX_QUERY || m < 1 || m > 12 || d < 1 || d > 31) return undefined;
  const date = finDelDia ? new Date(y, m - 1, d, 23, 59, 59, 999) : new Date(y, m - 1, d, 0, 0, 0, 0);
  return isNaN(date.getTime()) ? undefined : date;
}
