/**
 * Repositorios: persistencia del bundle (PULL) y operaciones de lectura para la UI.
 * El manejo del outbox vive en outbox.ts.
 */
import type { SQLiteDatabase } from 'expo-sqlite';
import { getDb } from './database';
import {
  Bundle,
  DeletedTombstones,
  Feature,
  Inspection,
  Issue,
  IssueComment,
  IssueTask,
  Layer,
  Media,
  Phase,
  Project,
  Template,
} from '../api/types';

const CURSOR_PREFIX = 'cursor:';
const ACTIVE_PROJECT_KEY = 'active_project';

// ---------- meta ----------

export async function getMeta(key: string): Promise<string | null> {
  const db = await getDb();
  const row = await db.getFirstAsync<{ value: string }>(
    'SELECT value FROM meta WHERE key = ?',
    key,
  );
  return row?.value ?? null;
}

export async function setMeta(key: string, value: string): Promise<void> {
  const db = await getDb();
  await db.runAsync(
    'INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
    key,
    value,
  );
}

export const getCursor = (projectId: number) => getMeta(CURSOR_PREFIX + projectId);
export const setCursor = (projectId: number, serverTime: string) =>
  setMeta(CURSOR_PREFIX + projectId, serverTime);

export const getActiveProjectId = async (): Promise<number | null> => {
  const v = await getMeta(ACTIVE_PROJECT_KEY);
  return v ? Number(v) : null;
};
export const setActiveProjectId = (id: number) => setMeta(ACTIVE_PROJECT_KEY, String(id));

// ---------- upsert genérico (preserva columnas locales) ----------

/**
 * INSERT ... ON CONFLICT(id) DO UPDATE: sólo toca las columnas del servidor,
 * así no pisa local_uuid/dirty de la fila.
 */
async function upsertById(
  db: SQLiteDatabase,
  table: string,
  row: Record<string, unknown>,
): Promise<void> {
  const cols = Object.keys(row);
  const placeholders = cols.map(() => '?').join(', ');
  const quoted = cols.map((c) => `"${c}"`).join(', ');
  const updates = cols
    .filter((c) => c !== 'id')
    .map((c) => `"${c}" = excluded."${c}"`)
    .join(', ');
  const sql =
    `INSERT INTO ${table} (${quoted}) VALUES (${placeholders}) ` +
    `ON CONFLICT(id) DO UPDATE SET ${updates}`;
  await db.runAsync(sql, ...cols.map((c) => row[c] as never));
}

const json = (v: unknown) => (v == null ? null : JSON.stringify(v));

// ---------- aplicar bundle ----------

/**
 * Aplica un bundle (snapshot o delta) a la BD local en una transacción:
 * upsert de cada entidad + borrado de tombstones + guardado del cursor.
 */
export async function applyBundle(bundle: Bundle): Promise<void> {
  const db = await getDb();
  const pid = bundle.project?.id;

  await db.withTransactionAsync(async () => {
    if (bundle.project) {
      await upsertById(db, 'projects', pickProject(bundle.project));
    }

    for (const p of bundle.phases ?? []) {
      await upsertById(db, 'phases', { ...pickPhase(p), project_id: pid });
    }
    for (const l of bundle.layers ?? []) {
      await upsertById(db, 'layers', { ...pickLayer(l), project_id: pid });
    }
    for (const f of bundle.features ?? []) {
      await upsertById(db, 'features', { ...pickFeature(f), project_id: pid });
    }
    for (const i of bundle.inspections ?? []) {
      await upsertById(db, 'inspections', { ...pickInspection(i), project_id: pid });
    }
    for (const i of bundle.issues ?? []) {
      await upsertById(db, 'issues', { ...pickIssue(i), project_id: pid });
    }
    for (const t of (bundle.issue_tasks ?? []) as IssueTask[]) {
      await upsertById(db, 'issue_tasks', pickIssueTask(t));
    }
    for (const c of bundle.issue_comments ?? []) {
      await upsertById(db, 'issue_comments', pickIssueComment(c));
    }
    for (const t of bundle.templates ?? []) {
      await upsertById(db, 'templates', pickTemplate(t));
    }
    for (const m of bundle.media ?? []) {
      await upsertById(db, 'media', {
        id: m.id,
        uuid: m.uuid,
        parent_entity: m.parent_entity,
        parent_id: m.parent_id,
        url: m.url,
        name: m.name ?? null,
        file_type: m.file_type ?? null,
        category: m.category ?? null,
        updated_at: m.updated_at,
      });
    }

    await applyTombstones(db, bundle.deleted);

    if (pid != null && bundle.server_time) {
      await db.runAsync(
        'INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
        CURSOR_PREFIX + pid,
        bundle.server_time,
      );
    }
  });
}

async function applyTombstones(db: SQLiteDatabase, deleted?: DeletedTombstones): Promise<void> {
  if (!deleted) return;
  const map: Record<keyof DeletedTombstones, string> = {
    phases: 'phases',
    layers: 'layers',
    features: 'features',
    inspections: 'inspections',
    issues: 'issues',
    issue_tasks: 'issue_tasks',
    issue_comments: 'issue_comments',
  };
  for (const key of Object.keys(map) as (keyof DeletedTombstones)[]) {
    const ids = deleted[key];
    if (ids && ids.length) {
      const placeholders = ids.map(() => '?').join(', ');
      await db.runAsync(`DELETE FROM ${map[key]} WHERE id IN (${placeholders})`, ...ids);
    }
  }
}

// ---------- proyección a columnas (sólo lo que persistimos) ----------

const pickProject = (p: Project) => ({
  id: p.id,
  reference: p.reference ?? null,
  name: p.name,
  address: p.address ?? null,
  lat: p.lat ?? null,
  lng: p.lng ?? null,
  status: p.status ?? null,
  updated_at: p.updated_at,
});

const pickPhase = (p: Phase) => ({
  id: p.id,
  name: p.name,
  order: p.order ?? null,
  color: p.color ?? null,
  progress_percent: p.progress_percent ?? null,
  updated_at: p.updated_at,
});

const pickLayer = (l: Layer) => ({
  id: l.id,
  phase_id: l.phase_id,
  name: l.name,
  color: l.color ?? null,
  updated_at: l.updated_at,
});

const pickFeature = (f: Feature) => ({
  id: f.id,
  layer_id: f.layer_id,
  name: f.name,
  geometry: json(f.geometry),
  status: f.status ?? null,
  progress: f.progress ?? null,
  responsible: f.responsible ?? null,
  template_id: f.template_id ?? null,
  updated_at: f.updated_at,
});

const pickInspection = (i: import('../api/types').Inspection) => ({
  id: i.id,
  feature_id: i.feature_id,
  layer_id: i.layer_id ?? null,
  template_id: i.template_id ?? null,
  user_id: i.user_id ?? null,
  data: json(i.data),
  status: i.status ?? null,
  result: i.result ?? null,
  notes: i.notes ?? null,
  created_at: i.created_at ?? null,
  updated_at: i.updated_at,
});

const pickIssue = (i: Issue) => ({
  id: i.id,
  feature_id: i.feature_id ?? null,
  title: i.title,
  description: i.description ?? null,
  status: i.status ?? null,
  priority: i.priority ?? null,
  type: i.type ?? null,
  reported_by: i.reported_by ?? null,
  assigned_to: i.assigned_to ?? null,
  resolved_at: i.resolved_at ?? null,
  updated_at: i.updated_at,
});

const pickIssueTask = (t: IssueTask) => ({
  id: t.id,
  issue_id: t.issue_id,
  title: t.title,
  is_done: t.is_done ? 1 : 0,
  done_at: t.done_at ?? null,
  done_by: t.done_by ?? null,
  assigned_to: t.assigned_to ?? null,
  due_date: t.due_date ?? null,
  order: t.order ?? null,
  updated_at: t.updated_at,
});

const pickIssueComment = (c: IssueComment) => ({
  id: c.id,
  issue_id: c.issue_id,
  user_id: c.user_id ?? null,
  body: c.body,
  created_at: c.created_at ?? null,
  updated_at: c.updated_at,
});

const pickTemplate = (t: Template) => ({
  id: t.id,
  project_id: t.project_id ?? null,
  phase_id: t.phase_id ?? null,
  name: t.name,
  description: t.description ?? null,
  fields: json(t.fields),
  version: String(t.version),
  hash: t.hash ?? null,
  updated_at: t.updated_at,
});

// ---------- lecturas para la UI ----------

export async function getProjects(): Promise<Project[]> {
  const db = await getDb();
  return db.getAllAsync<Project>('SELECT * FROM projects ORDER BY name');
}

export async function getProject(id: number): Promise<Project | null> {
  const db = await getDb();
  return db.getFirstAsync<Project>('SELECT * FROM projects WHERE id = ?', id);
}

/** Persiste la lista que devuelve GET /projects (upsert sin tocar el resto). */
export async function saveProjectList(projects: Project[]): Promise<void> {
  const db = await getDb();
  await db.withTransactionAsync(async () => {
    for (const p of projects) {
      await upsertById(db, 'projects', pickProject(p));
    }
  });
}

export async function getPhases(projectId: number): Promise<Phase[]> {
  const db = await getDb();
  return db.getAllAsync<Phase>(
    'SELECT * FROM phases WHERE project_id = ? ORDER BY "order", id',
    projectId,
  );
}

export async function getIssues(projectId: number): Promise<Issue[]> {
  const db = await getDb();
  return db.getAllAsync<Issue>(
    'SELECT * FROM issues WHERE project_id = ? ORDER BY updated_at DESC',
    projectId,
  );
}

export async function getIssue(id: number): Promise<Issue | null> {
  const db = await getDb();
  return db.getFirstAsync<Issue>('SELECT * FROM issues WHERE id = ?', id);
}

export async function getLayers(projectId: number): Promise<Layer[]> {
  const db = await getDb();
  return db.getAllAsync<Layer>(
    'SELECT * FROM layers WHERE project_id = ? ORDER BY name',
    projectId,
  );
}

/** Features de un proyecto. `geometry` se devuelve como objeto ya parseado. */
export async function getFeatures(projectId: number): Promise<Feature[]> {
  const db = await getDb();
  const rows = await db.getAllAsync<Feature & { geometry: string | null }>(
    'SELECT * FROM features WHERE project_id = ? ORDER BY layer_id, name',
    projectId,
  );
  return rows.map(parseGeometry);
}

export async function getFeature(id: number): Promise<Feature | null> {
  const db = await getDb();
  const row = await db.getFirstAsync<Feature & { geometry: string | null }>(
    'SELECT * FROM features WHERE id = ?',
    id,
  );
  return row ? parseGeometry(row) : null;
}

function parseGeometry(row: Feature & { geometry: string | null }): Feature {
  let geometry: unknown = null;
  if (row.geometry && typeof row.geometry === 'string') {
    try {
      geometry = JSON.parse(row.geometry);
    } catch {
      geometry = null;
    }
  }
  return { ...row, geometry };
}

export async function getInspectionsByFeature(featureId: number): Promise<Inspection[]> {
  const db = await getDb();
  const rows = await db.getAllAsync<Inspection & { data: string | null }>(
    'SELECT * FROM inspections WHERE feature_id = ? ORDER BY created_at DESC, id DESC',
    featureId,
  );
  return rows.map((r) => ({
    ...r,
    data: r.data ? (JSON.parse(r.data) as Record<string, unknown>) : undefined,
  }));
}

type IssueTaskRow = Omit<IssueTask, 'is_done'> & { is_done: number };

export async function getIssueTasks(issueId: number): Promise<IssueTask[]> {
  const db = await getDb();
  const rows = await db.getAllAsync<IssueTaskRow>(
    'SELECT * FROM issue_tasks WHERE issue_id = ? ORDER BY "order", id',
    issueId,
  );
  return rows.map((r) => ({ ...r, is_done: !!r.is_done }));
}

export async function getIssueComments(issueId: number): Promise<IssueComment[]> {
  const db = await getDb();
  return db.getAllAsync<IssueComment>(
    'SELECT * FROM issue_comments WHERE issue_id = ? ORDER BY created_at, id',
    issueId,
  );
}

export async function getTemplates(projectId: number): Promise<Template[]> {
  const db = await getDb();
  const rows = await db.getAllAsync<Template & { fields: string | null }>(
    'SELECT * FROM templates WHERE project_id = ? OR project_id IS NULL ORDER BY name',
    projectId,
  );
  return rows.map((r) => ({
    ...r,
    fields: r.fields ? (JSON.parse(r.fields) as unknown[]) : [],
  }));
}

export async function getTemplate(id: number): Promise<Template | null> {
  const db = await getDb();
  const row = await db.getFirstAsync<Template & { fields: string | null }>(
    'SELECT * FROM templates WHERE id = ?',
    id,
  );
  return row
    ? { ...row, fields: row.fields ? (JSON.parse(row.fields) as unknown[]) : [] }
    : null;
}

/** Media YA sincronizada (con url) para un padre. */
export async function getMediaFor(
  parentEntity: string,
  parentId: number,
): Promise<Media[]> {
  const db = await getDb();
  return db.getAllAsync<Media>(
    'SELECT * FROM media WHERE parent_entity = ? AND parent_id = ? ORDER BY id DESC',
    parentEntity,
    parentId,
  );
}

export async function countRows(table: string): Promise<number> {
  const db = await getDb();
  const row = await db.getFirstAsync<{ n: number }>(`SELECT COUNT(*) AS n FROM ${table}`);
  return row?.n ?? 0;
}

/** Aplica el valor del servidor (recibido en un conflicto) a la fila local. */
export async function applyServerValue(
  table: string,
  id: number,
  serverValue: Record<string, unknown>,
): Promise<void> {
  const db = await getDb();
  await upsertById(db, table, { id, ...serverValue });
}

// ---------- creaciones offline (optimista + reconciliación) ----------

/** Entidad de creación → tabla local. */
const CREATE_TABLE: Record<string, string> = {
  issue: 'issues',
  inspection: 'inspections',
  issue_task: 'issue_tasks',
  issue_comment: 'issue_comments',
};

/** Inserta una fila creada offline con un id temporal negativo y su local_uuid. */
export async function insertLocalCreate(
  entity: string,
  tempId: number,
  localUuid: string,
  row: Record<string, unknown>,
): Promise<void> {
  const table = CREATE_TABLE[entity];
  if (!table) return;
  const db = await getDb();
  const full: Record<string, unknown> = { id: tempId, local_uuid: localUuid, ...row };
  const cols = Object.keys(full);
  const sql = `INSERT INTO ${table} (${cols.map((c) => `"${c}"`).join(', ')}) VALUES (${cols
    .map(() => '?')
    .join(', ')})`;
  await db.runAsync(sql, ...cols.map((c) => full[c] as never));
}

/**
 * Sustituye el id temporal de una fila creada offline por el id real del
 * servidor y re-apunta las FKs de sus hijos (tareas/comentarios/fotos en cola).
 * Devuelve el id temporal que tenía (para que el motor remapee el outbox).
 */
export async function reconcileCreate(
  entity: string,
  localUuid: string,
  serverId: number,
): Promise<number | null> {
  const table = CREATE_TABLE[entity];
  if (!table) return null;
  const db = await getDb();

  const found = await db.getFirstAsync<{ id: number }>(
    `SELECT id FROM ${table} WHERE local_uuid = ?`,
    localUuid,
  );
  if (!found) return null;
  const tempId = found.id;

  await db.withTransactionAsync(async () => {
    if (tempId !== serverId) {
      await db.runAsync(`UPDATE ${table} SET id = ?, local_uuid = NULL WHERE id = ?`, serverId, tempId);
      // Hijos de una incidencia creada offline.
      if (entity === 'issue') {
        await db.runAsync('UPDATE issue_tasks SET issue_id = ? WHERE issue_id = ?', serverId, tempId);
        await db.runAsync('UPDATE issue_comments SET issue_id = ? WHERE issue_id = ?', serverId, tempId);
      }
      // Fotos en cola que apuntaban a esta fila por su id temporal.
      await db.runAsync(
        'UPDATE media_outbox SET parent_id = ? WHERE parent_entity = ? AND parent_id = ?',
        serverId,
        entity,
        tempId,
      );
    } else {
      await db.runAsync(`UPDATE ${table} SET local_uuid = NULL WHERE id = ?`, serverId);
    }
  });

  return tempId;
}
