"use client";

import { borrarToken, leerToken } from "./sesion";

export const URL_API =
  process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8091";

/**
 * Error que ya trae un mensaje escrito para que lo lea una persona.
 *
 * El backend contesta `{error: true, mensaje: "..."}` y esos mensajes están redactados
 * para quien está frente a la pantalla, no para un log. Se muestran tal cual.
 */
export class ErrorDeApi extends Error {
  constructor(
    mensaje: string,
    public readonly estado: number,
    public readonly referencia?: string,
  ) {
    super(mensaje);
    this.name = "ErrorDeApi";
  }
}

export class SesionVencida extends ErrorDeApi {
  constructor(mensaje: string) {
    super(mensaje, 401);
    this.name = "SesionVencida";
  }
}

type Opciones = {
  metodo?: "GET" | "POST" | "PUT" | "DELETE";
  cuerpo?: unknown;
};

export async function api<T>(ruta: string, opciones: Opciones = {}): Promise<T> {
  const token = leerToken();

  const cabeceras: Record<string, string> = {
    "Content-Type": "application/json",
  };
  if (token) cabeceras.Authorization = `Bearer ${token}`;

  let respuesta: Response;
  try {
    respuesta = await fetch(`${URL_API}${ruta}`, {
      method: opciones.metodo ?? "GET",
      headers: cabeceras,
      body: opciones.cuerpo === undefined ? undefined : JSON.stringify(opciones.cuerpo),
      cache: "no-store",
    });
  } catch {
    // Distinguir «no llegué al servidor» de «el servidor dijo que no» importa: la
    // primera se arregla mirando la red o el servicio, la segunda mirando los datos.
    throw new ErrorDeApi(
      `No se pudo contactar el servidor de Producción en ${URL_API}. ` +
        `Revisá que esté arriba y que la dirección sea la correcta.`,
      0,
    );
  }

  let datos: unknown = null;
  const texto = await respuesta.text();
  if (texto) {
    try {
      datos = JSON.parse(texto);
    } catch {
      datos = null;
    }
  }

  const cuerpo = datos as
    | { error?: boolean; mensaje?: string; datos?: T; referencia?: string }
    | null;

  if (respuesta.status === 401) {
    borrarToken();
    throw new SesionVencida(
      cuerpo?.mensaje ??
        "La sesión venció. Volvé a abrir Producción desde el Back Office.",
    );
  }

  if (!respuesta.ok || cuerpo?.error) {
    throw new ErrorDeApi(
      cuerpo?.mensaje ?? `El servidor respondió ${respuesta.status}.`,
      respuesta.status,
      cuerpo?.referencia,
    );
  }

  return (cuerpo?.datos ?? (cuerpo as unknown)) as T;
}

// ── Tipos que devuelve la API ────────────────────────────────────────────────

export type Sesion = {
  usuario: number;
  nombre: string;
  permisos: string[];
  administrador: boolean;
  /** A dónde se vuelve al Back Office. La manda mbinv al emitir la sesión. */
  retorno: string | null;
};

export type Bodega = {
  tienda: number;
  descripcion: string;
  usaProduccion: string;
  esTransito: boolean;
};

export type Linea = {
  id: number;
  codigo: string;
  nombre: string;
  activa: boolean;
  orden: number;
  bodegaMateriaPrima: number;
  bodegaProduccion: number;
  bodegaProductoTerminado: number;
  bodegaDespacho: number | null;
  entradaTerminadoEn: "PROCESO" | "DESPACHO" | "DESTINO";
  nombreBodegas: {
    materiaPrima: string | null;
    produccion: string | null;
    productoTerminado: string | null;
    despacho: string | null;
  };
};

export type EstadoOrden =
  | "BORRADOR"
  | "PLANIFICADA"
  | "MP_DESPACHADA"
  | "EN_EJECUCION"
  | "PRODUCIDA"
  | "TERMINADA"
  | "ANULADA";

export type ResumenOrden = {
  id: number;
  fecha: string;
  estado: EstadoOrden;
  estadoEtiqueta: string;
  linea: string | null;
  lineaId: number | null;
  productos: number;
  despachoDocumento: string | null;
  produccionDocumento: string | null;
  trasladoDocumento: string | null;
};

export type DetalleProducto = {
  plu: string;
  descripcion: string | null;
  cantidadPlanificada: number;
  cantidadProducida: number;
  diferencia: number;
  unidadesPorLote: number | null;
  lotesEquivalentes: number | null;
  nota: string | null;
};

export type Insumo = {
  plu: string;
  descripcion: string | null;
  cantidadRequerida: number;
  cantidadDespachada: number;
  existenciaAlPlanificar: number;
  faltante: number;
};

export type Orden = ResumenOrden & {
  bodegaMateriaPrimaNombre: string | null;
  bodegaProduccionNombre: string | null;
  bodegaProductoTerminadoNombre: string | null;
  observaciones: string | null;
  bodegaMateriaPrima: number;
  bodegaProduccion: number;
  bodegaProductoTerminado: number;
  bodegaDespacho: number;
  entradaTerminadoEn: "PROCESO" | "DESPACHO" | "DESTINO";
  /** Todos los documentos que la orden dejó en mbinv: bodega que lo emitió, tipo y número. */
  entradaImplicaTraslado: boolean;
  trasladoPendienteReal: boolean;
  cerradoEn: string | null;
  costoEstimadoTotal: number | null;
  costoRealTotal: number | null;
  costoDiferencia: number | null;
  documentosInventario: { tipodoc: number; documento: string; bodega: number; origen: number | null; destino: number | null }[];
  transicionesPosibles: { valor: EstadoOrden; etiqueta: string }[];
  detalles: DetalleProducto[];
  insumos: Insumo[];
  creadoEn: string;
};

export type Producto = {
  plu: string;
  descripcion: string;
  tieneFormula: boolean;
};

export type VentaHistorica = {
  plu: string;
  descripcion: string;
  cantidad: number;
};

export type EstadoConsumoAdicional = "PENDIENTE" | "DESPACHADO" | "CONSUMIDO" | "ANULADO";

/**
 * Materia prima que la planta gastó por encima de la fórmula para completar el lote.
 *
 * `pluProducto` es a cuál de los productos de la orden se le carga el costo, y no es
 * adorno: una orden con dos productos gasta de más por uno de los dos, y sin esa columna
 * el costeo por producto se vuelve un promedio que no le sirve a nadie.
 */
export type ConsumoAdicional = {
  id: number;
  pluProducto: string;
  pluInsumo: string;
  descripcionInsumo: string | null;
  cantidad: number;
  motivo: string;
  anuladoMotivo: string | null;
  requiereDespacho: boolean;
  estado: EstadoConsumoAdicional;
  despachoDocumento: string | null;
  consumoDocumento: string | null;
  existenciaAlAgregar: number;
  hayFaltante: boolean;
  creadoPor: number;
  creadoEn: string;
};

export type ConsumosAdicionalesDeOrden = {
  orden: number;
  estado: EstadoOrden;
  consumos: ConsumoAdicional[];
};

export type TipoCosto = "MANO_OBRA" | "GASTO_FABRICACION" | "INDIRECTO";
export type BaseCosto = "UNIDAD_PRODUCIDA" | "MANUAL";

export type RenglonCosto = {
  id: number;
  pluProducto: string;
  codigo: string;
  descripcion: string;
  tipo: TipoCosto;
  base: BaseCosto;
  tarifa: number;
  baseEstimada: number;
  /** Base manual que nadie capturó todavía. No se deduce del número: un cero capturado es un dato válido. */
  basePendiente: boolean;
  montoEstimado: number;
  baseReal: number | null;
  montoReal: number | null;
};

export type BloqueCosto = {
  etiqueta: string;
  estimado: number;
  real: number | null;
  diferencia: number | null;
};

export type ProductoCosto = {
  plu: string;
  producida: number;
  materialesReal: number | null;
  componentesEstimado: number;
  componentesReal: number | null;
  componentesDiferencia: number | null;
  costoUnitarioConComponentes: number | null;
  costoUnitarioMateriales: number | null;
};

/**
 * El costo de la orden por componentes, como la cotización de Envaisa: materiales, mano de
 * obra, gastos de fabricación e indirectos, en estimado y real.
 *
 * `componentesEnInventario` es el dato que más importa y el que más fácil se lee mal: dice
 * si esos números llegaron al costo con el que el producto terminado entró al inventario,
 * o si se quedaron en la pantalla.
 */
export type CostosDeOrden = {
  orden: number;
  estado: EstadoOrden;
  componentesEnInventario: boolean;
  /** Si la orden ya se estimó alguna vez, aunque no haya dejado renglones (ninguna tarifa aplicaba). */
  yaEstimada: boolean;
  bloques: Record<string, BloqueCosto>;
  total: {
    estimado: number;
    estimadoCompleto: boolean;
    real: number | null;
    diferencia: number | null;
  };
  productos: ProductoCosto[];
  renglones: RenglonCosto[];
  avisos: string[];
};

// ── Catálogo de tarifas de costo ─────────────────────────────────────────────

export type TarifaCosto = {
  id: number;
  codigo: string;
  descripcion: string;
  tipo: TipoCosto;
  tipoEtiqueta: string;
  tarifa: number;
  base: BaseCosto;
  lineaId: number | null;
  pluProducto: string | null;
  /** Por qué se creó o cambió la tarifa: obligatorio en el servidor. */
  motivo: string | null;
  vigenteDesde: string;
  vigenteHasta: string | null;
  activo: boolean;
};

export type ResultadoCargaMasiva = {
  aplicado: boolean;
  filasValidas: number;
  errores: { fila: number; mensaje: string }[];
  creados: number[];
};

/**
 * Sube el texto de un CSV de tarifas. `aplicar = false` sólo lo revisa.
 *
 * No pasa por `api()` a propósito: el servidor contesta 422 cuando el archivo trae errores y
 * lo que hay que mostrar son esos errores (fila por fila), que viajan en `datos` y no en
 * `mensaje`. El cuerpo son los BYTES del archivo, sin decodificar: el servidor sabe leer UTF-8 y
 * Windows-1252 (Excel en español), y pasar el archivo por `File.text()` lo decodificaría siempre como
 * UTF-8 y estropearía las tildes antes de que lleguen.
 */
export async function cargarTarifasCsv(csv: ArrayBuffer, aplicar: boolean): Promise<ResultadoCargaMasiva> {
  const token = leerToken();
  const cabeceras: Record<string, string> = { "Content-Type": "text/csv" };
  if (token) cabeceras.Authorization = `Bearer ${token}`;

  let respuesta: Response;
  try {
    respuesta = await fetch(`${URL_API}/api/costos-estandar/carga-masiva${aplicar ? "?aplicar=1" : ""}`, {
      method: "POST",
      headers: cabeceras,
      body: csv,
      cache: "no-store",
    });
  } catch {
    throw new ErrorDeApi(`No se pudo contactar el servidor de Producción en ${URL_API}.`, 0);
  }

  if (respuesta.status === 401) {
    borrarToken();
    throw new SesionVencida("La sesión venció. Volvé a abrir Producción desde el Back Office.");
  }

  let cuerpo: { error?: boolean; mensaje?: string; datos?: ResultadoCargaMasiva } | null = null;
  try {
    cuerpo = JSON.parse(await respuesta.text());
  } catch {
    cuerpo = null;
  }

  if (cuerpo?.datos) return cuerpo.datos;

  throw new ErrorDeApi(cuerpo?.mensaje ?? `El servidor respondió ${respuesta.status}.`, respuesta.status);
}

// ── Consumo real, ejecución y ajustes tardíos (P3, P10) ──────────────────────

export type ReglaConsumo = "PROPORCIONAL" | "FIJO_LOTE" | "CAPTURADO";

export type LineaConsumoReal = {
  pluProducto: string;
  pluComponente: string;
  descripcion: string | null;
  regla: ReglaConsumo;
  esperadoPlanificado: number;
  esperadoSegunProduccion: number | null;
  real: number | null;
  capturado: boolean;
  /** Lo gastado de más (positivo) o de menos (negativo) que lo que correspondía con lo producido: el uso. */
  desviacionPorUso: number | null;
  /** Lo que correspondía con lo producido contra lo planificado: el rendimiento. */
  desviacionPorRendimiento: number | null;
};

export type ConsumosRealesDeOrden = {
  orden: number;
  estado: EstadoOrden;
  formula: LineaConsumoReal[];
  adicionales: { id: number; pluProducto: string; pluInsumo: string; descripcion: string | null; cantidad: number; motivo: string; estado: string }[];
};

export type TipoAjuste = "MATERIALES" | "MANO_OBRA" | "GASTO_FABRICACION" | "INDIRECTO";

export type AjusteTardio = {
  id: number;
  pluProducto: string;
  tipo: TipoAjuste;
  concepto: string;
  /** TEXTO decimal exacto («12.500000»): un número JSON cambiaría los últimos dígitos. */
  valor: string;
  motivo: string;
  fecha: string;
  creadoPor: number;
  creadoEn: string;
};

export type AjustesDeOrden = {
  orden: number;
  cerradoEn: string | null;
  costoRealCongelado: string | null;
  totalAjustes: string;
  costoRealAjustado: string | null;
  porProducto: Record<string, string>;
  ajustes: AjusteTardio[];
  avisos: string[];
};
