// Native storage implementation backed by expo-sqlite (SDK 56 async API).
//
// Mirrors the web/Dexie repo's contract exactly. The full transcript body is
// stored as JSON in the 'json' column; metadata lives in dedicated columns so
// list()/search() can be answered from columns alone without parsing every
// body. Search uses a LIKE over the lowercased 'searchText' column.
//
// API reference: https://docs.expo.dev/versions/v56.0.0/sdk/sqlite/
// We use openDatabaseAsync + the async methods (execAsync/runAsync/getAllAsync).

import * as SQLite from 'expo-sqlite';
import type { Segment } from '../types';
import { newId } from '../ids';
import {
  parseDraft,
  zSegment,
  type TranscriptDraft,
  type TranscriptMeta,
  type Transcript,
} from './schema';
import type { StorageRepo } from './repo';

// Row shape as returned by getAllAsync from the metadata columns. SQLite has no
// boolean/undefined: optional language comes back as string | null.
interface MetaRow {
  id: string;
  title: string;
  createdAt: number;
  updatedAt: number;
  durationSec: number;
  segmentCount: number;
  modelId: string;
  language: string | null;
}

// A meta row plus the JSON body, for get().
interface FullRow extends MetaRow {
  json: string;
}

// ---------------------------------------------------------------------------
// Pure derivation helpers
// ---------------------------------------------------------------------------

/** Lowercased haystack of title + every segment's text, for LIKE search. */
function deriveSearchText(title: string, segments: Segment[]): string {
  return [title, ...segments.map((s) => s.text)].join('\n').toLowerCase();
}

/** Map a metadata row to the public TranscriptMeta (drop null language). */
function rowToMeta(r: MetaRow): TranscriptMeta {
  return {
    id: r.id,
    title: r.title,
    createdAt: r.createdAt,
    updatedAt: r.updatedAt,
    durationSec: r.durationSec,
    modelId: r.modelId as TranscriptMeta['modelId'],
    segmentCount: r.segmentCount,
    ...(r.language !== null ? { language: r.language } : {}),
  };
}

// ---------------------------------------------------------------------------
// Lazy, memoized database handle + schema bootstrap
// ---------------------------------------------------------------------------

let dbPromise: Promise<SQLite.SQLiteDatabase> | undefined;

function getDb(): Promise<SQLite.SQLiteDatabase> {
  // Open once and reuse; create the table on first open. The async open returns
  // a promise we cache so concurrent callers share a single connection.
  if (!dbPromise) {
    dbPromise = (async () => {
      const db = await SQLite.openDatabaseAsync('wisp.db');
      await db.execAsync(`
        CREATE TABLE IF NOT EXISTS transcripts (
          id TEXT PRIMARY KEY NOT NULL,
          json TEXT NOT NULL,
          searchText TEXT NOT NULL,
          createdAt INTEGER NOT NULL,
          updatedAt INTEGER NOT NULL,
          durationSec REAL NOT NULL,
          segmentCount INTEGER NOT NULL,
          modelId TEXT NOT NULL,
          language TEXT
        );
        CREATE INDEX IF NOT EXISTS idx_transcripts_createdAt
          ON transcripts (createdAt);
      `);
      return db;
    })();
  }
  return dbPromise;
}

// The metadata columns we select for list()/search(), kept in one place.
const META_COLUMNS =
  'id, title, createdAt, updatedAt, durationSec, segmentCount, modelId, language';

// ---------------------------------------------------------------------------
// Repo implementation
// ---------------------------------------------------------------------------

export const repo: StorageRepo = {
  async list(): Promise<TranscriptMeta[]> {
    const db = await getDb();
    const rows = await db.getAllAsync<MetaRow>(
      `SELECT ${META_COLUMNS} FROM transcripts ORDER BY createdAt DESC`,
    );
    return rows.map(rowToMeta);
  },

  async get(id: string): Promise<Transcript | undefined> {
    const db = await getDb();
    const row = await db.getFirstAsync<FullRow>(
      `SELECT ${META_COLUMNS}, json FROM transcripts WHERE id = ?`,
      [id],
    );
    if (!row) return undefined;
    // The JSON body is the authoritative full Transcript (already validated on
    // write), so we return it directly rather than reconstructing from columns.
    return JSON.parse(row.json) as Transcript;
  },

  async create(draft: TranscriptDraft): Promise<Transcript> {
    const valid = parseDraft(draft);
    const now = Date.now();
    const transcript: Transcript = {
      id: newId('t_'),
      title: valid.title,
      createdAt: now,
      updatedAt: now,
      durationSec: valid.durationSec,
      modelId: valid.modelId,
      ...(valid.language !== undefined ? { language: valid.language } : {}),
      segmentCount: valid.segments.length,
      segments: valid.segments,
    };

    const db = await getDb();
    await db.runAsync(
      `INSERT INTO transcripts
         (id, json, searchText, createdAt, updatedAt, durationSec, segmentCount, modelId, language)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
      [
        transcript.id,
        JSON.stringify(transcript),
        deriveSearchText(transcript.title, transcript.segments),
        transcript.createdAt,
        transcript.updatedAt,
        transcript.durationSec,
        transcript.segmentCount,
        transcript.modelId,
        // expo-sqlite accepts null but not undefined for params.
        transcript.language ?? null,
      ],
    );
    return transcript;
  },

  async update(
    id: string,
    patch: Partial<Pick<Transcript, 'title' | 'segments'>>,
  ): Promise<Transcript> {
    const db = await getDb();
    const existingRow = await db.getFirstAsync<FullRow>(
      `SELECT ${META_COLUMNS}, json FROM transcripts WHERE id = ?`,
      [id],
    );
    if (!existingRow) {
      throw new Error(`transcript not found: ${id}`);
    }
    const existing = JSON.parse(existingRow.json) as Transcript;

    // Re-validate segments on update so non-finite times can never slip in.
    const nextSegments =
      patch.segments !== undefined
        ? patch.segments.map((s) => zSegment.parse(s))
        : existing.segments;
    const nextTitle = patch.title !== undefined ? patch.title : existing.title;

    const updated: Transcript = {
      ...existing,
      title: nextTitle,
      segments: nextSegments,
      segmentCount: nextSegments.length,
      updatedAt: Date.now(),
    };

    await db.runAsync(
      `UPDATE transcripts
         SET json = ?, searchText = ?, updatedAt = ?, segmentCount = ?, title = ?
       WHERE id = ?`,
      [
        JSON.stringify(updated),
        deriveSearchText(nextTitle, nextSegments),
        updated.updatedAt,
        updated.segmentCount,
        updated.title,
        id,
      ],
    );
    return updated;
  },

  async remove(id: string): Promise<void> {
    const db = await getDb();
    await db.runAsync(`DELETE FROM transcripts WHERE id = ?`, [id]);
  },

  async search(query: string): Promise<TranscriptMeta[]> {
    const db = await getDb();
    const needle = query.toLowerCase().trim();
    if (!needle) {
      // Empty query => everything, newest first (matches the web repo).
      const rows = await db.getAllAsync<MetaRow>(
        `SELECT ${META_COLUMNS} FROM transcripts ORDER BY createdAt DESC`,
      );
      return rows.map(rowToMeta);
    }
    // Escape LIKE wildcards (% and _) and the escape char itself so user input
    // is matched literally, then use ESCAPE to declare the escape character.
    const escaped = needle.replace(/[\\%_]/g, (ch) => `\\${ch}`);
    const rows = await db.getAllAsync<MetaRow>(
      `SELECT ${META_COLUMNS} FROM transcripts
         WHERE searchText LIKE ? ESCAPE '\\'
       ORDER BY createdAt DESC`,
      [`%${escaped}%`],
    );
    return rows.map(rowToMeta);
  },
};
