/**
 * LocalMemoryStore — SQLite-backed fallback for chat memory when
 * Temper isn't configured. Implements the same surface as TemperClient
 * (see lib/chat/memory-store.ts MemoryStore interface) so callers don't
 * branch on the backend.
 *
 * Two tables under workflow.db:
 *   memory_blocks   — key/value pairs (Temper "blocks")
 *   memory_episodes — append-only event log (Temper "episodes")
 *
 * Differences from Temper:
 *   - search() does case-insensitive LIKE over block values + episode
 *     content. No embeddings, no graph extraction.
 *   - writeEpisode() returns immediately; there is no async extraction.
 *   - Namespaces are honored at write/read time, but `scope=global` is
 *     a synonym for `scope=own` (local has a single tenant).
 */

import { join } from 'node:path';
import { getDb } from '../../src/core/db/database';
import { getDataDir } from '../dirs';
import type Database from 'better-sqlite3';
import type {
  EpisodeInput,
  MemoryBlock,
  MemoryStore,
  SearchHit,
} from './memory-store';

const DEFAULT_NAMESPACE = 'local';

interface BlockRow {
  ns: string;
  key: string;
  value: string;          // JSON-encoded
  pinned: number;         // 0/1
  priority: number | null;
  description: string | null;
  scope: string;          // 'own' | 'global'
  updated_at: string;     // ISO
}

interface EpisodeRow {
  id: number;
  ns: string;
  content: string;
  tags: string | null;    // JSON-encoded array
  source_type: string | null;
  reference_time: string | null;
  created_at: string;     // ISO
}

let schemaReady = false;

function db(): Database.Database {
  return getDb(join(getDataDir(), 'workflow.db'));
}

function ensureSchema(): void {
  if (schemaReady) return;
  const conn = db();
  conn.exec(`
    CREATE TABLE IF NOT EXISTS memory_blocks (
      ns          TEXT NOT NULL,
      key         TEXT NOT NULL,
      value       TEXT NOT NULL,
      pinned      INTEGER NOT NULL DEFAULT 0,
      priority    INTEGER,
      description TEXT,
      scope       TEXT NOT NULL DEFAULT 'own',
      updated_at  TEXT NOT NULL,
      PRIMARY KEY (ns, key)
    );
    CREATE INDEX IF NOT EXISTS idx_memory_blocks_pinned
      ON memory_blocks(ns, pinned);

    CREATE TABLE IF NOT EXISTS memory_episodes (
      id             INTEGER PRIMARY KEY AUTOINCREMENT,
      ns             TEXT NOT NULL,
      content        TEXT NOT NULL,
      tags           TEXT,
      source_type    TEXT,
      reference_time TEXT,
      created_at     TEXT NOT NULL
    );
    CREATE INDEX IF NOT EXISTS idx_memory_episodes_ns_created
      ON memory_episodes(ns, created_at DESC);
  `);
  schemaReady = true;
}

function nowIso(): string {
  return new Date().toISOString();
}

function decodeValue(raw: string): unknown {
  try {
    return JSON.parse(raw);
  } catch {
    return raw;
  }
}

function rowToBlock(r: BlockRow): MemoryBlock {
  return {
    key: r.key,
    value: decodeValue(r.value),
    pinned: !!r.pinned,
    priority: r.priority ?? undefined,
    description: r.description ?? undefined,
    scope: (r.scope as 'own' | 'global') ?? 'own',
  };
}

export class LocalMemoryStore implements MemoryStore {
  readonly kind = 'local' as const;
  private readonly ns: string;

  constructor(namespace?: string) {
    this.ns = (namespace || DEFAULT_NAMESPACE).trim() || DEFAULT_NAMESPACE;
    ensureSchema();
  }

  get enabled(): boolean {
    return true;
  }

  get currentNamespace(): string {
    return this.ns;
  }

  async search(query: string, limit = 8): Promise<SearchHit[]> {
    const q = (query || '').trim();
    if (!q) return [];
    const cap = Math.min(50, Math.max(1, limit));
    const like = `%${q.replace(/[%_]/g, (m) => '\\' + m)}%`;
    const conn = db();

    const blockHits = conn.prepare(
      `SELECT key, value, description, updated_at
         FROM memory_blocks
        WHERE ns = ?
          AND (value LIKE ? ESCAPE '\\' OR key LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\')
        ORDER BY pinned DESC, updated_at DESC
        LIMIT ?`,
    ).all(this.ns, like, like, like, cap) as Array<Pick<BlockRow, 'key' | 'value' | 'description' | 'updated_at'>>;

    const episodeHits = conn.prepare(
      `SELECT id, content, reference_time, created_at
         FROM memory_episodes
        WHERE ns = ?
          AND content LIKE ? ESCAPE '\\'
        ORDER BY created_at DESC
        LIMIT ?`,
    ).all(this.ns, like, cap) as Array<Pick<EpisodeRow, 'id' | 'content' | 'reference_time' | 'created_at'>>;

    const hits: SearchHit[] = [];
    for (const b of blockHits) {
      const valueStr = typeof decodeValue(b.value) === 'string'
        ? String(decodeValue(b.value))
        : b.value;
      hits.push({
        id: `block:${b.key}`,
        kind: 'fact',
        fact: `${b.key}: ${truncate(valueStr, 200)}`,
        valid_at: b.updated_at,
        invalid_at: null,
        namespace: this.ns,
      });
    }
    for (const e of episodeHits) {
      hits.push({
        id: `episode:${e.id}`,
        kind: 'fact',
        fact: truncate(e.content, 300),
        valid_at: e.reference_time ?? e.created_at,
        invalid_at: null,
        namespace: this.ns,
      });
    }
    return hits.slice(0, cap);
  }

  async writeEpisode(ep: EpisodeInput, _asyncExtract = true): Promise<boolean> {
    if (!ep.content || !ep.content.trim()) return false;
    try {
      const conn = db();
      conn.prepare(
        `INSERT INTO memory_episodes(ns, content, tags, source_type, reference_time, created_at)
         VALUES (?, ?, ?, ?, ?, ?)`,
      ).run(
        ep.namespace || this.ns,
        ep.content,
        ep.tags && ep.tags.length ? JSON.stringify(ep.tags) : null,
        ep.source_type ?? 'text',
        ep.reference_time ?? nowIso(),
        nowIso(),
      );
      return true;
    } catch (err) {
      console.warn('[local-memory] writeEpisode failed', err);
      return false;
    }
  }

  async listBlocks(opts: { pinned?: boolean; scope?: 'own' | 'global' | 'both' } = {}): Promise<MemoryBlock[]> {
    try {
      const conn = db();
      const where: string[] = ['ns = ?'];
      const params: unknown[] = [this.ns];
      if (opts.pinned) where.push('pinned = 1');
      // scope=both means no filter; scope=own/global filters on stored value.
      // Local store treats global ≡ own (single tenant), so we only filter when
      // the caller explicitly asked for one or the other AND any rows actually
      // have that scope. Otherwise return all.
      const sql = `SELECT ns, key, value, pinned, priority, description, scope, updated_at
                     FROM memory_blocks
                    WHERE ${where.join(' AND ')}
                    ORDER BY pinned DESC, updated_at DESC`;
      const rows = conn.prepare(sql).all(...params) as BlockRow[];
      return rows.map(rowToBlock);
    } catch (err) {
      console.warn('[local-memory] listBlocks failed', err);
      return [];
    }
  }

  async getBlock(key: string, _scope: 'own' | 'global' = 'own'): Promise<MemoryBlock | null> {
    try {
      const conn = db();
      const row = conn.prepare(
        `SELECT ns, key, value, pinned, priority, description, scope, updated_at
           FROM memory_blocks
          WHERE ns = ? AND key = ?`,
      ).get(this.ns, key) as BlockRow | undefined;
      if (!row) return null;
      return rowToBlock(row);
    } catch (err) {
      console.warn('[local-memory] getBlock failed', err);
      return null;
    }
  }

  async putBlock(
    key: string,
    value: unknown,
    extras: { pinned?: boolean; priority?: number; description?: string; scope?: 'own' | 'global' } = {},
  ): Promise<boolean> {
    if (!key) return false;
    try {
      const conn = db();
      conn.prepare(
        `INSERT INTO memory_blocks(ns, key, value, pinned, priority, description, scope, updated_at)
              VALUES (?, ?, ?, ?, ?, ?, ?, ?)
         ON CONFLICT(ns, key) DO UPDATE SET
            value       = excluded.value,
            pinned      = excluded.pinned,
            priority    = excluded.priority,
            description = excluded.description,
            scope       = excluded.scope,
            updated_at  = excluded.updated_at`,
      ).run(
        this.ns,
        key,
        JSON.stringify(value),
        extras.pinned ? 1 : 0,
        extras.priority ?? null,
        extras.description ?? null,
        extras.scope ?? 'own',
        nowIso(),
      );
      return true;
    } catch (err) {
      console.warn('[local-memory] putBlock failed', err);
      return false;
    }
  }

  async ping(): Promise<{ ok: boolean; message: string; pinned: number }> {
    try {
      const conn = db();
      const row = conn.prepare(
        `SELECT COUNT(*) AS n FROM memory_blocks WHERE ns = ? AND pinned = 1`,
      ).get(this.ns) as { n: number };
      const n = row?.n ?? 0;
      return {
        ok: true,
        message: `Local memory · ${n} pinned block${n === 1 ? '' : 's'}`,
        pinned: n,
      };
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      return { ok: false, message: `Local memory unavailable: ${msg}`, pinned: 0 };
    }
  }
}

function truncate(s: string, max: number): string {
  return s.length > max ? s.slice(0, max) + '…' : s;
}
