/**
 * Outbox: cola de salida de operaciones (/sync) y de ficheros (/media).
 * Cada cambio offline se encola aquí; el motor de sync la vacía con red.
 */
import { getDb } from './database';
import { Operation, OperationResult, SyncEntity, SyncOp } from '../api/types';
import { newUuid, nowIso } from '../sync/uuid';

export interface OutboxRow extends Operation {
  status: 'pending' | 'sent' | 'conflict' | 'error';
  server_id: number | null;
  error: string | null;
  server_payload: string | null;
  attempts: number;
  created_at: string;
}

/** Encola una operación. Devuelve su uuid (idempotencia). */
export async function enqueueOperation(
  entity: SyncEntity,
  op: SyncOp,
  data: Record<string, unknown>,
  opts?: { uuid?: string; localId?: number },
): Promise<string> {
  const db = await getDb();
  const uuid = opts?.uuid ?? newUuid();
  const clientUpdatedAt = nowIso();
  await db.runAsync(
    `INSERT INTO outbox (uuid, entity, op, data, client_updated_at, status, attempts, created_at, local_id)
     VALUES (?, ?, ?, ?, ?, 'pending', 0, ?, ?)`,
    uuid,
    entity,
    op,
    JSON.stringify(data),
    clientUpdatedAt,
    clientUpdatedAt,
    opts?.localId ?? null,
  );
  return uuid;
}

interface RawOutbox {
  uuid: string;
  entity: SyncEntity;
  op: SyncOp;
  data: string;
  client_updated_at: string;
  status: OutboxRow['status'];
  server_id: number | null;
  error: string | null;
  server_payload: string | null;
  attempts: number;
  created_at: string;
  local_id: number | null;
}

/** Operación pendiente enriquecida con su id local temporal (si la creó offline). */
export interface PendingOp extends Operation {
  /** id temporal (negativo) de la fila local que produjo esta operación, o null. */
  localId: number | null;
}

/** Operaciones pendientes de enviar, en orden de creación. */
export async function getPendingOutbox(limit = 200): Promise<PendingOp[]> {
  const db = await getDb();
  const rows = await db.getAllAsync<RawOutbox>(
    `SELECT * FROM outbox WHERE status = 'pending' ORDER BY created_at ASC LIMIT ?`,
    limit,
  );
  return rows.map((r) => ({
    entity: r.entity,
    op: r.op,
    uuid: r.uuid,
    client_updated_at: r.client_updated_at,
    data: JSON.parse(r.data) as Record<string, unknown>,
    localId: r.local_id,
  }));
}

/** Reescribe el `data` de una operación pendiente (tras remapear ids temporales). */
export async function updateOutboxData(
  uuid: string,
  data: Record<string, unknown>,
): Promise<void> {
  const db = await getDb();
  await db.runAsync('UPDATE outbox SET data = ? WHERE uuid = ?', JSON.stringify(data), uuid);
}

/** Aplica el resultado de /sync de una operación a su fila del outbox. */
export async function applyOperationResult(result: OperationResult): Promise<void> {
  const db = await getDb();
  if (result.status === 'applied' || result.status === 'duplicate') {
    await db.runAsync(
      `UPDATE outbox SET status = 'sent', server_id = ?, error = NULL WHERE uuid = ?`,
      result.server_id ?? null,
      result.uuid,
    );
  } else if (result.status === 'conflict') {
    await db.runAsync(
      `UPDATE outbox SET status = 'conflict', server_payload = ?, attempts = attempts + 1 WHERE uuid = ?`,
      result.server ? JSON.stringify(result.server) : null,
      result.uuid,
    );
  } else {
    await db.runAsync(
      `UPDATE outbox SET status = 'error', error = ?, attempts = attempts + 1 WHERE uuid = ?`,
      result.error ?? 'unknown error',
      result.uuid,
    );
  }
}

/** Limpia las operaciones ya confirmadas (housekeeping opcional). */
export async function purgeSentOperations(): Promise<void> {
  const db = await getDb();
  await db.runAsync(`DELETE FROM outbox WHERE status = 'sent'`);
}

export interface ProblemOp {
  uuid: string;
  entity: SyncEntity;
  op: SyncOp;
  status: 'conflict' | 'error';
  error: string | null;
  server_payload: string | null;
  data: string;
  created_at: string;
}

/** Operaciones que requieren atención (conflicto o error). */
export async function getProblemOps(): Promise<ProblemOp[]> {
  const db = await getDb();
  return db.getAllAsync<ProblemOp>(
    `SELECT uuid, entity, op, status, error, server_payload, data, created_at
     FROM outbox WHERE status IN ('conflict', 'error') ORDER BY created_at DESC`,
  );
}

/** Reintentar: vuelve a poner la operación como pendiente. */
export async function retryOp(uuid: string): Promise<void> {
  const db = await getDb();
  await db.runAsync(
    `UPDATE outbox SET status = 'pending', error = NULL, server_payload = NULL WHERE uuid = ?`,
    uuid,
  );
}

/** Descartar: elimina la operación del outbox. */
export async function discardOp(uuid: string): Promise<void> {
  const db = await getDb();
  await db.runAsync('DELETE FROM outbox WHERE uuid = ?', uuid);
}

export interface OutboxCounts {
  pending: number;
  conflict: number;
  error: number;
  mediaPending: number;
}

export async function getOutboxCounts(): Promise<OutboxCounts> {
  const db = await getDb();
  const r = await db.getFirstAsync<{ pending: number; conflict: number; error: number }>(
    `SELECT
       SUM(status='pending')  AS pending,
       SUM(status='conflict') AS conflict,
       SUM(status='error')    AS error
     FROM outbox`,
  );
  const m = await db.getFirstAsync<{ n: number }>(
    `SELECT COUNT(*) AS n FROM media_outbox WHERE status = 'pending'`,
  );
  return {
    pending: r?.pending ?? 0,
    conflict: r?.conflict ?? 0,
    error: r?.error ?? 0,
    mediaPending: m?.n ?? 0,
  };
}

// ---------- media outbox ----------

export interface EnqueueMediaInput {
  parentEntity: string;
  parentId: number;
  localUri: string;
  fileName?: string;
  mimeType?: string;
  category?: 'image' | 'document' | 'other';
  description?: string;
}

export async function enqueueMedia(input: EnqueueMediaInput): Promise<string> {
  const db = await getDb();
  const uuid = newUuid();
  await db.runAsync(
    `INSERT INTO media_outbox
       (uuid, parent_entity, parent_id, local_uri, file_name, mime_type, category, description, status, attempts, created_at)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?)`,
    uuid,
    input.parentEntity,
    input.parentId,
    input.localUri,
    input.fileName ?? null,
    input.mimeType ?? null,
    input.category ?? null,
    input.description ?? null,
    nowIso(),
  );
  return uuid;
}

export interface MediaOutboxRow {
  uuid: string;
  parent_entity: string;
  parent_id: number;
  local_uri: string;
  file_name: string | null;
  mime_type: string | null;
  category: 'image' | 'document' | 'other' | null;
  description: string | null;
  status: 'pending' | 'sent' | 'error';
  media_id: number | null;
  error: string | null;
  attempts: number;
}

export async function getPendingMedia(limit = 50): Promise<MediaOutboxRow[]> {
  const db = await getDb();
  // parent_id > 0: el padre ya tiene id de servidor (los creados offline aún
  // tienen id temporal negativo; esperan a reconciliarse).
  return db.getAllAsync<MediaOutboxRow>(
    `SELECT * FROM media_outbox WHERE status = 'pending' AND parent_id > 0 ORDER BY created_at ASC LIMIT ?`,
    limit,
  );
}

/** Media local (pendiente o con error) de un padre, para previsualizar en la UI. */
export async function getMediaOutboxFor(
  parentEntity: string,
  parentId: number,
): Promise<MediaOutboxRow[]> {
  const db = await getDb();
  return db.getAllAsync<MediaOutboxRow>(
    `SELECT * FROM media_outbox WHERE parent_entity = ? AND parent_id = ? AND status != 'sent' ORDER BY created_at DESC`,
    parentEntity,
    parentId,
  );
}

export async function markMediaSent(uuid: string, mediaId: number | null): Promise<void> {
  const db = await getDb();
  await db.runAsync(
    `UPDATE media_outbox SET status = 'sent', media_id = ?, error = NULL WHERE uuid = ?`,
    mediaId,
    uuid,
  );
}

export async function markMediaError(uuid: string, error: string): Promise<void> {
  const db = await getDb();
  await db.runAsync(
    `UPDATE media_outbox SET status = 'error', error = ?, attempts = attempts + 1 WHERE uuid = ?`,
    error,
    uuid,
  );
}
