import * as SQLite from "expo-sqlite";
import type {
  LocalInterventionRecord,
  LocalInterventionsSnapshot,
  SyncQueueItem,
  SyncQueueMethod,
} from "../types/offline";
import type {
  MobileIntervention,
  MobileInterventionDetails,
  MobileStep,
} from "../types/intervention";

const DB_NAME = "missioflow-mobile.db";

let dbPromise: Promise<SQLite.SQLiteDatabase> | null = null;

async function getDb(): Promise<SQLite.SQLiteDatabase> {
  dbPromise ??= SQLite.openDatabaseAsync(DB_NAME);
  return dbPromise;
}

async function getTableColumns(tableName: string): Promise<Set<string>> {
  const db = await getDb();
  const rows = await db.getAllAsync<{ name: string }>(`PRAGMA table_info(${tableName})`);
  return new Set(rows.map((row) => row.name));
}

async function upsertLocalMeta(key: string, value: string, now: string): Promise<void> {
  const db = await getDb();
  await db.runAsync(
    `INSERT INTO local_meta (key, value, updated_at)
     VALUES (?, ?, ?)
     ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
    [key, value, now]
  );
}

function safeIso(value: string | null | undefined): string {
  const parsed = value ? Date.parse(value) : Number.NaN;
  return Number.isNaN(parsed) ? "1970-01-01T00:00:00.000Z" : new Date(parsed).toISOString();
}

async function ensureStepsSchema(): Promise<void> {
  const db = await getDb();
  const columns = await getTableColumns("steps");

  // Migration from legacy schema (step_key/payload blob) to structured columns.
  // Files column = miniatures photos remontees par mobile/intervention_steps.php
  // pour l'affichage lecture seule, persistees pour survivre a un cold start.
  const STEPS_COLUMNS: Array<[string, string]> = [
    ["step_id", "TEXT"],
    ["order_index", "INTEGER NOT NULL DEFAULT 0"],
    ["type", "TEXT NOT NULL DEFAULT 'text'"],
    ["label", "TEXT NOT NULL DEFAULT ''"],
    ["required", "INTEGER NOT NULL DEFAULT 0"],
    ["status", "TEXT NOT NULL DEFAULT 'todo'"],
    ["value_json", "TEXT"],
    ["unit", "TEXT"],
    ["constraints_json", "TEXT"],
    ["options_json", "TEXT"],
    ["metadata_json", "TEXT"],
    ["files_json", "TEXT"],
    // Visibilite serveur (contrat #168). NULL = inconnu (ancienne donnee) ->
    // traite comme visible a la lecture. 1 = visible, 0 = masquee.
    ["is_visible", "INTEGER"],
    ["version", "INTEGER NOT NULL DEFAULT 1"],
    ["dirty", "INTEGER NOT NULL DEFAULT 0"],
  ];

  for (const [name, def] of STEPS_COLUMNS) {
    if (!columns.has(name)) {
      await db.runAsync(`ALTER TABLE steps ADD COLUMN ${name} ${def}`);
    }
  }

  await db.execAsync(`
    UPDATE steps
    SET step_id = COALESCE(step_id, step_key)
    WHERE step_id IS NULL AND step_key IS NOT NULL;

    UPDATE steps
    SET value_json = payload
    WHERE value_json IS NULL AND payload IS NOT NULL;

    UPDATE steps
    SET step_id = CAST(id AS TEXT)
    WHERE step_id IS NULL OR TRIM(step_id) = '';

    DELETE FROM steps
    WHERE id IN (
      SELECT s1.id
      FROM steps s1
      JOIN steps s2
        ON s1.intervention_id = s2.intervention_id
       AND s1.step_id = s2.step_id
       AND s1.id < s2.id
    );

    CREATE INDEX IF NOT EXISTS idx_steps_intervention
    ON steps(intervention_id, order_index);

    CREATE INDEX IF NOT EXISTS idx_steps_dirty
    ON steps(dirty, intervention_id);

    CREATE UNIQUE INDEX IF NOT EXISTS uq_steps_intervention_step
    ON steps(intervention_id, step_id);

    CREATE TABLE IF NOT EXISTS step_conflicts (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      intervention_id INTEGER NOT NULL,
      step_id TEXT NOT NULL,
      local_value_json TEXT,
      server_value_json TEXT,
      local_version INTEGER,
      server_version INTEGER,
      created_at TEXT NOT NULL
    );
  `);
}

export async function initializeLocalDatabase(): Promise<void> {
  const db = await getDb();

  await db.execAsync(`
    PRAGMA journal_mode = WAL;

    CREATE TABLE IF NOT EXISTS local_meta (
      key TEXT PRIMARY KEY NOT NULL,
      value TEXT,
      updated_at TEXT NOT NULL
    );

    CREATE TABLE IF NOT EXISTS missions (
      id INTEGER PRIMARY KEY NOT NULL,
      payload TEXT NOT NULL,
      updated_at TEXT NOT NULL
    );

    CREATE TABLE IF NOT EXISTS interventions (
      id INTEGER PRIMARY KEY NOT NULL,
      payload TEXT NOT NULL,
      updated_at TEXT NOT NULL
    );

    CREATE TABLE IF NOT EXISTS steps (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      intervention_id INTEGER NOT NULL,
      step_key TEXT,
      payload TEXT,
      step_id TEXT,
      order_index INTEGER NOT NULL DEFAULT 0,
      type TEXT NOT NULL DEFAULT 'text',
      label TEXT NOT NULL DEFAULT '',
      required INTEGER NOT NULL DEFAULT 0,
      status TEXT NOT NULL DEFAULT 'todo',
      value_json TEXT,
      unit TEXT,
      constraints_json TEXT,
      options_json TEXT,
      metadata_json TEXT,
      version INTEGER NOT NULL DEFAULT 1,
      dirty INTEGER NOT NULL DEFAULT 0,
      updated_at TEXT NOT NULL
    );

    CREATE TABLE IF NOT EXISTS sync_queue (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      endpoint TEXT NOT NULL,
      method TEXT NOT NULL,
      payload TEXT NOT NULL,
      entity_type TEXT NOT NULL,
      entity_id TEXT NOT NULL,
      status TEXT NOT NULL DEFAULT 'pending',
      attempts INTEGER NOT NULL DEFAULT 0,
      last_error TEXT,
      created_at TEXT NOT NULL,
      updated_at TEXT NOT NULL
    );

    CREATE TABLE IF NOT EXISTS step_conflicts (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      intervention_id INTEGER NOT NULL,
      step_id TEXT NOT NULL,
      local_value_json TEXT,
      server_value_json TEXT,
      local_version INTEGER,
      server_version INTEGER,
      created_at TEXT NOT NULL
    );
  `);

  await ensureStepsSchema();
}

export async function clearLocalDatabase(): Promise<void> {
  const db = await getDb();
  await db.execAsync(`
    DELETE FROM missions;
    DELETE FROM interventions;
    DELETE FROM steps;
    DELETE FROM step_conflicts;
    DELETE FROM sync_queue;
    DELETE FROM local_meta;
  `);
}

export async function saveOrUpdateStep(step: MobileStep, dirty = false): Promise<void> {
  const db = await getDb();

  let isVisibleColumn: number | null;
  if (step.is_visible === undefined || step.is_visible === null) {
    isVisibleColumn = null;
  } else {
    isVisibleColumn = step.is_visible ? 1 : 0;
  }

  await db.runAsync(
    `INSERT INTO steps (
      intervention_id,
      step_id,
      order_index,
      type,
      label,
      required,
      status,
      value_json,
      unit,
      constraints_json,
      options_json,
      metadata_json,
      files_json,
      is_visible,
      version,
      dirty,
      updated_at
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    ON CONFLICT(intervention_id, step_id)
    DO UPDATE SET
      order_index = excluded.order_index,
      type = excluded.type,
      label = excluded.label,
      required = excluded.required,
      status = excluded.status,
      value_json = excluded.value_json,
      unit = excluded.unit,
      constraints_json = excluded.constraints_json,
      options_json = excluded.options_json,
      metadata_json = excluded.metadata_json,
      files_json = excluded.files_json,
      is_visible = excluded.is_visible,
      version = excluded.version,
      dirty = excluded.dirty,
      updated_at = excluded.updated_at`,
    [
      step.intervention_id,
      step.id,
      step.order_index,
      step.type,
      step.label,
      step.required ? 1 : 0,
      step.status,
      JSON.stringify(step.value ?? null),
      step.unit ?? null,
      JSON.stringify(step.constraints ?? {}),
      JSON.stringify(step.options ?? []),
      JSON.stringify(step.metadata ?? {}),
      JSON.stringify(step.files ?? []),
      isVisibleColumn,
      step.version,
      dirty ? 1 : 0,
      safeIso(step.updated_at),
    ]
  );
}

export async function applyServerStepsDelta(steps: MobileStep[]): Promise<number> {
  const db = await getDb();
  let applied = 0;

  await db.withTransactionAsync(async () => {
    for (const step of steps) {
      const local = await db.getFirstAsync<{
        value_json: string | null;
        version: number;
        updated_at: string;
        dirty: number;
      }>(
        `SELECT value_json, version, updated_at, dirty
         FROM steps
         WHERE intervention_id = ? AND step_id = ?`,
        [step.intervention_id, step.id]
      );

      let shouldApply = !local;
      if (local) {
        const localVersion = Number(local.version || 0);
        const serverVersion = Number(step.version || 0);

        if (serverVersion > localVersion) {
          shouldApply = true;
        } else if (serverVersion === localVersion) {
          shouldApply = safeIso(step.updated_at) > safeIso(local.updated_at);
        }

        if (shouldApply && Number(local.dirty || 0) === 1) {
          await db.runAsync(
            `INSERT INTO step_conflicts (
              intervention_id, step_id, local_value_json, server_value_json,
              local_version, server_version, created_at
            ) VALUES (?, ?, ?, ?, ?, ?, ?)`,
            [
              step.intervention_id,
              step.id,
              local.value_json,
              JSON.stringify(step.value ?? null),
              localVersion,
              serverVersion,
              new Date().toISOString(),
            ]
          );
        }
      }

      if (shouldApply) {
        await saveOrUpdateStep(step, false);
        applied += 1;
      }
    }
  });

  return applied;
}

export async function saveInterventionsSnapshot(items: MobileIntervention[]): Promise<void> {
  const db = await getDb();
  const now = new Date().toISOString();

  await db.withTransactionAsync(async () => {
    await db.runAsync("DELETE FROM missions");

    for (const item of items) {
      await db.runAsync(
        "INSERT INTO missions (id, payload, updated_at) VALUES (?, ?, ?)",
        [item.id, JSON.stringify(item), now]
      );
    }

    await upsertLocalMeta("missions_snapshot_count", String(items.length), now);
    await upsertLocalMeta("missions_snapshot_updated_at", now, now);
  });
}

/**
 * Fusion NON destructive du snapshot interventions, pour le delta de
 * synchronisation (sync_pull.php). Contrairement a saveInterventionsSnapshot
 * (qui vide puis recree la table), on :
 *   - upsert chaque intervention changee (INSERT OR REPLACE par id) ;
 *   - purge les interventions absentes de `keepIds` (reassignees a un autre
 *     technicien, supprimees, hors fenetre serveur).
 *
 * `keepIds` = liste COMPLETE des ids cote serveur (pas seulement les changes).
 *   - null  -> aucune purge (payload partiel / pas d'info de purge) ;
 *   - []    -> le tech n'a plus aucune intervention -> on vide la table.
 *
 * Sans ce merge, nourrir saveInterventionsSnapshot avec un delta effacerait
 * toutes les interventions non modifiees (DELETE FROM missions global).
 */
export async function mergeInterventionsSnapshot(
  items: MobileIntervention[],
  keepIds: number[] | null
): Promise<void> {
  const db = await getDb();
  const now = new Date().toISOString();

  await db.withTransactionAsync(async () => {
    for (const item of items) {
      await db.runAsync(
        "INSERT OR REPLACE INTO missions (id, payload, updated_at) VALUES (?, ?, ?)",
        [item.id, JSON.stringify(item), now]
      );
    }

    if (keepIds) {
      if (keepIds.length === 0) {
        await db.runAsync("DELETE FROM missions");
      } else {
        const placeholders = keepIds.map(() => "?").join(", ");
        await db.runAsync(
          `DELETE FROM missions WHERE id NOT IN (${placeholders})`,
          keepIds
        );
      }
    }

    const countRow = await db.getFirstAsync<{ c: number }>(
      "SELECT COUNT(*) as c FROM missions"
    );
    const count = Number(countRow?.c ?? 0);
    await upsertLocalMeta("missions_snapshot_count", String(count), now);
    await upsertLocalMeta("missions_snapshot_updated_at", now, now);
  });
}

export async function getInterventionsSnapshot(): Promise<LocalInterventionsSnapshot> {
  const db = await getDb();
  const rows = await db.getAllAsync<{ id: number; payload: string; updated_at: string }>(
    "SELECT id, payload, updated_at FROM missions ORDER BY id DESC"
  );

  const items: MobileIntervention[] = [];
  for (const row of rows) {
    try {
      items.push(JSON.parse(row.payload) as MobileIntervention);
    } catch {
      // Ignore malformed rows.
    }
  }

  const updatedRow = await db.getFirstAsync<{ value: string }>(
    "SELECT value FROM local_meta WHERE key = 'missions_snapshot_updated_at'"
  );

  return {
    items,
    count: items.length,
    updated_at: updatedRow?.value || "",
  };
}

export async function saveInterventionDetail(item: MobileInterventionDetails): Promise<void> {
  const db = await getDb();
  const now = new Date().toISOString();

  await db.runAsync(
    `INSERT INTO interventions (id, payload, updated_at)
     VALUES (?, ?, ?)
     ON CONFLICT(id) DO UPDATE SET payload=excluded.payload, updated_at=excluded.updated_at`,
    [item.id, JSON.stringify(item), now]
  );
}

export async function getInterventionDetailById(
  interventionId: number
): Promise<LocalInterventionRecord | null> {
  const db = await getDb();
  const row = await db.getFirstAsync<{ payload: string; updated_at: string }>(
    "SELECT payload, updated_at FROM interventions WHERE id = ?",
    [interventionId]
  );

  if (!row) {
    return null;
  }

  try {
    return {
      item: JSON.parse(row.payload) as MobileInterventionDetails,
      updated_at: row.updated_at,
    };
  } catch {
    return null;
  }
}

/**
 * Purge locale d'une intervention (et ses etapes). Utilisee par la sync en
 * arriere-plan (couche 2) sur un data push action="delete" : l'intervention a
 * ete supprimee/reassignee cote serveur, on retire son cache local pour ne pas
 * la presenter au tech. Ne touche pas la file de sync (une ecriture en attente
 * sur une intervention supprimee mourra proprement cote serveur).
 */
export async function deleteInterventionLocally(interventionId: number): Promise<void> {
  const db = await getDb();
  await db.withTransactionAsync(async () => {
    await db.runAsync("DELETE FROM steps WHERE intervention_id = ?", [interventionId]);
    await db.runAsync("DELETE FROM interventions WHERE id = ?", [interventionId]);
    await db.runAsync("DELETE FROM missions WHERE id = ?", [interventionId]);
  });
}

export async function enqueueSyncAction(input: {
  endpoint: string;
  method: SyncQueueMethod;
  payload: Record<string, unknown>;
  entityType: string;
  entityId: string;
}): Promise<void> {
  const db = await getDb();
  const now = new Date().toISOString();

  // M5 — coalescence : si une action de MEME type est deja en file 'pending' pour
  // la meme entite (ex. un save_step deja enfile pour ce step), on REMPLACE son
  // payload (derniere valeur gagne) au lieu d'empiler un doublon. Sans ca, ree-
  // diter une etape hors-ligne creait 2 POST (la 1re valeur etait meme rejouee
  // inutilement). On restreint a (entity_type, entity_id, action) IDENTIQUES pour
  // ne JAMAIS fusionner deux actions differentes (ex. intervention_start vs
  // complete_workflow sur la meme intervention). Les payloads multipart (photos,
  // sans `action`) ne sont jamais coalesces.
  const action =
    typeof input.payload?.action === "string" ? input.payload.action : null;
  if (action !== null) {
    const existing = await db.getFirstAsync<{ id: number; payload: string }>(
      `SELECT id, payload FROM sync_queue
       WHERE status = 'pending' AND entity_type = ? AND entity_id = ?
       ORDER BY id DESC LIMIT 1`,
      [input.entityType, input.entityId]
    );
    if (existing) {
      let sameAction = false;
      try {
        sameAction = (JSON.parse(existing.payload)?.action ?? null) === action;
      } catch {
        sameAction = false;
      }
      if (sameAction) {
        // On remplace le payload ET on REMET A ZERO le budget de retries : une
        // valeur fraiche ne doit pas heriter des tentatives/erreurs d'un envoi
        // precedent (sinon elle pourrait mourir prematurement au cap, ou rester
        // 'dead'/'failed'). On la repasse explicitement 'pending'.
        await db.runAsync(
          `UPDATE sync_queue
           SET payload = ?, status = 'pending', attempts = 0, last_error = NULL, updated_at = ?
           WHERE id = ?`,
          [JSON.stringify(input.payload), now, existing.id]
        );
        return;
      }
    }
  }

  await db.runAsync(
    `INSERT INTO sync_queue (
      endpoint, method, payload, entity_type, entity_id, status, attempts, created_at, updated_at
    ) VALUES (?, ?, ?, ?, ?, 'pending', 0, ?, ?)`,
    [
      input.endpoint,
      input.method,
      JSON.stringify(input.payload),
      input.entityType,
      input.entityId,
      now,
      now,
    ]
  );
}

export async function getPendingQueueItems(limit = 50): Promise<SyncQueueItem[]> {
  const db = await getDb();
  const rows = await db.getAllAsync<{
    id: number;
    endpoint: string;
    method: SyncQueueMethod;
    payload: string;
    entity_type: string;
    entity_id: string;
    created_at: string;
    attempts: number;
  }>(
    `SELECT id, endpoint, method, payload, entity_type, entity_id, created_at, attempts
     FROM sync_queue
     WHERE status = 'pending'
     ORDER BY id ASC
     LIMIT ?`,
    [limit]
  );

  const mapped: SyncQueueItem[] = [];
  for (const row of rows) {
    try {
      mapped.push({
        id: row.id,
        endpoint: row.endpoint,
        method: row.method,
        payload: JSON.parse(row.payload) as Record<string, unknown>,
        entity_type: row.entity_type,
        entity_id: row.entity_id,
        created_at: row.created_at,
        attempts: row.attempts,
      });
    } catch {
      // Skip malformed queue payload.
    }
  }

  return mapped;
}

export async function markQueueItemDone(id: number): Promise<void> {
  const db = await getDb();
  await db.runAsync("DELETE FROM sync_queue WHERE id = ?", [id]);
}

// Au-dela de ce nombre de tentatives, un item retryable (5xx / reseau) est
// bascule en 'dead' : il n'est plus rejoue (evite le retry infini d'un item
// qui echoue durablement et qui, en tete de file, bloquait tout le reste).
export const MAX_SYNC_ATTEMPTS = 8;

/**
 * Echec RETRYABLE (5xx, reseau) : incremente attempts + memorise l'erreur.
 * Quand attempts atteint MAX_SYNC_ATTEMPTS, l'item passe 'dead' (plus rejoue).
 */
export async function markQueueItemFailed(id: number, message: string): Promise<void> {
  const db = await getDb();
  const now = new Date().toISOString();
  await db.runAsync(
    `UPDATE sync_queue
     SET attempts = attempts + 1,
         last_error = ?,
         updated_at = ?,
         status = CASE WHEN attempts + 1 >= ? THEN 'dead' ELSE status END
     WHERE id = ?`,
    [message.slice(0, 500), now, MAX_SYNC_ATTEMPTS, id]
  );
}

/**
 * Echec DEFINITIF (erreur de contrat 400/404/409/422 propre a l'item) :
 * l'item est sorti de la file ('dead') sans nouvelle tentative. Sans ca un
 * 404 restait 'pending' et bloquait le drain de toute la file (ORDER BY id).
 */
export async function markQueueItemDead(id: number, message: string): Promise<void> {
  const db = await getDb();
  const now = new Date().toISOString();
  await db.runAsync(
    `UPDATE sync_queue
     SET status = 'dead',
         attempts = attempts + 1,
         last_error = ?,
         updated_at = ?
     WHERE id = ?`,
    [message.slice(0, 500), now, id]
  );
}

/**
 * M4 — Compte les actions de sync DEFINITIVEMENT en echec ('dead' : 409/422/413
 * residuels, cap de tentatives atteint). La sync etant decouplee de l'ecran, ces
 * echecs disparaissaient sans aucune trace UI ; ce compteur permet a App de
 * remonter un avertissement au technicien (ex. une finalisation ou une photo qui
 * n'est jamais passee).
 */
export async function countDeadQueueItems(): Promise<number> {
  const db = await getDb();
  const row = await db.getFirstAsync<{ c: number }>(
    `SELECT COUNT(*) as c FROM sync_queue WHERE status = 'dead'`
  );
  return Number(row?.c ?? 0);
}

/**
 * Mur de facturation (402 BILLING_WALL) : l'ecriture est refusee parce que
 * l'abonnement du tenant n'est plus actif. Ce N'EST PAS une erreur d'item :
 * l'intervention est valide, seul le paiement manque. On garde donc l'item
 * 'pending' SANS toucher 'attempts' (sinon il finirait 'dead' au cap
 * MAX_SYNC_ATTEMPTS comme un retryable, ce qui perdrait l'ecriture). Ainsi il
 * se re-poussera tel quel a la reactivation (frontiere de responsabilite, cf.
 * mur de facturation §3.2bis cote serveur). On memorise quand meme l'erreur.
 */
export async function markQueueItemWalled(id: number, message: string): Promise<void> {
  const db = await getDb();
  const now = new Date().toISOString();
  await db.runAsync(
    `UPDATE sync_queue
     SET last_error = ?,
         updated_at = ?
     WHERE id = ?`,
    [message.slice(0, 500), now, id]
  );
}

// Identite du technicien proprietaire du cache local courant. Permet de
// detecter, au login, qu'un AUTRE compte se connecte sur ce device et de
// purger les donnees du precedent (interventions, steps, file de sync, drafts)
// avant de charger les siennes -> pas de fuite de donnees entre comptes.
const OWNER_USER_ID_KEY = "owner_user_id";

export async function getLocalDbOwnerId(): Promise<number | null> {
  const db = await getDb();
  const row = await db.getFirstAsync<{ value: string }>(
    "SELECT value FROM local_meta WHERE key = ?",
    [OWNER_USER_ID_KEY]
  );
  const parsed = row?.value ? Number(row.value) : Number.NaN;
  return Number.isFinite(parsed) ? parsed : null;
}

export async function setLocalDbOwnerId(userId: number): Promise<void> {
  await upsertLocalMeta(OWNER_USER_ID_KEY, String(userId), new Date().toISOString());
}

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

export async function setLastSyncTimestamp(value: string): Promise<void> {
  await upsertLocalMeta("last_sync_at", value, new Date().toISOString());
}

/**
 * Draft complet d'un workflow d'intervention, persiste en local_meta sous
 * la cle `workflow_draft_<interventionId>`. Permet au tech de retrouver
 * son etat (valeurs, commentaires, signatures, phase courante) apres un
 * crash/kill de l'app. Aucune perte entre les relances tant que le device
 * n'est pas reset.
 */
export type WorkflowDraft = {
  inputValues: Record<string, unknown>;
  commentValues: Record<string, string>;
  currentPhaseIdx: number;
  sigTechData: string;
  sigClientData: string;
  sigClientPresent: boolean;
  showSignatureCard: boolean;
  updatedAt: string;
};

function draftKey(interventionId: number): string {
  return `workflow_draft_${interventionId}`;
}

export async function saveWorkflowDraft(
  interventionId: number,
  draft: WorkflowDraft
): Promise<void> {
  await upsertLocalMeta(
    draftKey(interventionId),
    JSON.stringify(draft),
    new Date().toISOString()
  );
}

export async function loadWorkflowDraft(
  interventionId: number
): Promise<WorkflowDraft | null> {
  const db = await getDb();
  const row = await db.getFirstAsync<{ value: string }>(
    "SELECT value FROM local_meta WHERE key = ?",
    [draftKey(interventionId)]
  );
  if (!row?.value) {
    return null;
  }
  try {
    return JSON.parse(row.value) as WorkflowDraft;
  } catch {
    return null;
  }
}

export async function clearWorkflowDraft(interventionId: number): Promise<void> {
  const db = await getDb();
  await db.runAsync("DELETE FROM local_meta WHERE key = ?", [draftKey(interventionId)]);
}

export async function getStepsByInterventionId(
  interventionId: number
): Promise<MobileStep[]> {
  const db = await getDb();
  const rows = await db.getAllAsync<{
    step_id: string;
    order_index: number;
    type: string;
    label: string;
    required: number;
    status: string;
    value_json: string | null;
    unit: string | null;
    constraints_json: string | null;
    options_json: string | null;
    metadata_json: string | null;
    files_json: string | null;
    is_visible: number | null;
    version: number;
    updated_at: string;
    dirty: number;
  }>(
    `SELECT step_id, order_index, type, label, required, status,
            value_json, unit, constraints_json, options_json,
            metadata_json, files_json, is_visible, version, updated_at, dirty
     FROM steps
     WHERE intervention_id = ?
     ORDER BY order_index ASC`,
    [interventionId]
  );

  return rows.map((row) => ({
    id: row.step_id,
    intervention_id: interventionId,
    order_index: row.order_index,
    type: row.type as MobileStep["type"],
    label: row.label,
    required: row.required === 1,
    status: row.status as MobileStep["status"],
    value: row.value_json
      ? (() => {
          try {
            return JSON.parse(row.value_json);
          } catch {
            return null;
          }
        })()
      : null,
    unit: row.unit ?? undefined,
    constraints: row.constraints_json
      ? (() => {
          try {
            return JSON.parse(row.constraints_json) as MobileStep["constraints"];
          } catch {
            return {};
          }
        })()
      : undefined,
    options: row.options_json
      ? (() => {
          try {
            return JSON.parse(row.options_json) as string[];
          } catch {
            return [];
          }
        })()
      : undefined,
    metadata: row.metadata_json
      ? (() => {
          try {
            return JSON.parse(row.metadata_json) as Record<string, unknown>;
          } catch {
            return {};
          }
        })()
      : undefined,
    files: row.files_json
      ? (() => {
          try {
            return JSON.parse(row.files_json) as MobileStep["files"];
          } catch {
            return [];
          }
        })()
      : [],
    is_visible:
      row.is_visible === null || row.is_visible === undefined
        ? undefined
        : row.is_visible === 1,
    version: row.version,
    updated_at: row.updated_at,
  }));
}

export async function updateStepValueLocally(
  interventionId: number,
  stepId: string,
  updates: {
    status: MobileStep["status"];
    value?: MobileStep["value"];
    version: number;
  }
): Promise<void> {
  const db = await getDb();
  const now = new Date().toISOString();

  await db.runAsync(
    `UPDATE steps
     SET status = ?,
         value_json = ?,
         version = ?,
         dirty = 1,
         updated_at = ?
     WHERE intervention_id = ? AND step_id = ?`,
    [
      updates.status,
      JSON.stringify(updates.value ?? null),
      updates.version,
      now,
      interventionId,
      stepId,
    ]
  );
}
