// Web storage implementation backed by Dexie (IndexedDB).
//
// We keep ONE object store ('transcripts') keyed by id. Each record holds the
// full Transcript plus a derived, lowercased 'searchText' (title + all segment
// text) used for case-insensitive substring search. Search is done in-memory
// (filter over all rows) because IndexedDB has no native substring index;
// transcript counts are small enough on-device that this is fine.

import Dexie from 'dexie';
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';

// The shape persisted in IndexedDB: a full Transcript plus the derived
// searchText column. searchText is never returned to callers.
interface StoredTranscript extends Transcript {
  searchText: string;
}

// ---------------------------------------------------------------------------
// Pure derivation helpers (shared by create/update within this file)
// ---------------------------------------------------------------------------

/** Lowercased haystack of title + every segment's text, for substring search. */
function deriveSearchText(title: string, segments: Segment[]): string {
  // Join with newlines so adjacent segments don't accidentally fuse into a
  // word that matches a query spanning the boundary.
  return [title, ...segments.map((s) => s.text)].join('\n').toLowerCase();
}

/** Strip the derived searchText, returning the public Transcript view. */
function toTranscript(row: StoredTranscript): Transcript {
  const { searchText: _searchText, ...rest } = row;
  return rest;
}

/** Strip both searchText and segments, returning the lightweight meta view. */
function toMeta(row: StoredTranscript): TranscriptMeta {
  const { searchText: _searchText, segments: _segments, ...meta } = row;
  return meta;
}

// ---------------------------------------------------------------------------
// Dexie database
// ---------------------------------------------------------------------------

class WispDexie extends Dexie {
  // Only 'id' and 'createdAt' need to be indexed: id is the primary key,
  // createdAt powers the newest-first ordering. searchText is matched by an
  // in-memory filter, so it does not need an index.
  transcripts!: Dexie.Table<StoredTranscript, string>;

  constructor() {
    super('wisp');
    this.version(1).stores({
      transcripts: 'id, createdAt',
    });
  }
}

const db = new WispDexie();

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

export const repo: StorageRepo = {
  async list(): Promise<TranscriptMeta[]> {
    // Sort by createdAt then reverse for newest-first.
    const rows = await db.transcripts.orderBy('createdAt').reverse().toArray();
    return rows.map(toMeta);
  },

  async get(id: string): Promise<Transcript | undefined> {
    const row = await db.transcripts.get(id);
    return row ? toTranscript(row) : undefined;
  },

  async create(draft: TranscriptDraft): Promise<Transcript> {
    // Validate+throw before doing anything else.
    const valid = parseDraft(draft);
    const now = Date.now();
    const stored: StoredTranscript = {
      id: newId('t_'),
      title: valid.title,
      createdAt: now,
      updatedAt: now,
      durationSec: valid.durationSec,
      modelId: valid.modelId,
      // Preserve optional language only when present (avoid storing undefined).
      ...(valid.language !== undefined ? { language: valid.language } : {}),
      segmentCount: valid.segments.length,
      segments: valid.segments,
      searchText: deriveSearchText(valid.title, valid.segments),
    };
    await db.transcripts.put(stored);
    return toTranscript(stored);
  },

  async update(
    id: string,
    patch: Partial<Pick<Transcript, 'title' | 'segments'>>,
  ): Promise<Transcript> {
    const existing = await db.transcripts.get(id);
    if (!existing) {
      throw new Error(`transcript not found: ${id}`);
    }

    // Re-validate segments on every write so an update can never persist
    // non-finite times that a create() would have rejected.
    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: StoredTranscript = {
      ...existing,
      title: nextTitle,
      segments: nextSegments,
      // Re-derive denormalized fields so search() and list() stay correct.
      segmentCount: nextSegments.length,
      searchText: deriveSearchText(nextTitle, nextSegments),
      updatedAt: Date.now(),
    };
    await db.transcripts.put(updated);
    return toTranscript(updated);
  },

  async remove(id: string): Promise<void> {
    await db.transcripts.delete(id);
  },

  async search(query: string): Promise<TranscriptMeta[]> {
    const needle = query.toLowerCase().trim();
    const rows = await db.transcripts.orderBy('createdAt').reverse().toArray();
    // Empty query => return everything (newest first), matching common list UX.
    const matched = needle
      ? rows.filter((r) => r.searchText.includes(needle))
      : rows;
    return matched.map(toMeta);
  },
};
