// Phase-1 intelligence memory. Uses node:sqlite when available and degrades to no-op.

import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { dirname, join } from 'path';
import { createHash } from 'crypto';

const PREDICTION_STATES = new Set(['open', 'monitoring', 'observed', 'expired_unverified', 'invalidated']);

export class IntelligenceStore {
  constructor(dbPath) {
    this.dbPath = dbPath;
    this.db = null;
    this.available = false;
    this.reason = null;
  }

  async init() {
    const dir = dirname(this.dbPath);
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
    try {
      const sqlite = await import('node:sqlite');
      const DatabaseSync = sqlite.DatabaseSync;
      this.db = new DatabaseSync(this.dbPath);
      this.db.exec(`
        CREATE TABLE IF NOT EXISTS runs (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          timestamp TEXT NOT NULL,
          sources_ok INTEGER DEFAULT 0,
          sources_degraded INTEGER DEFAULT 0,
          sources_failed INTEGER DEFAULT 0,
          direction TEXT,
          summary_json TEXT NOT NULL
        );
        CREATE TABLE IF NOT EXISTS predictions (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          stable_id TEXT UNIQUE,
          created_at TEXT NOT NULL,
          updated_at TEXT,
          title TEXT NOT NULL,
          type TEXT,
          hypothesis TEXT,
          evidence_json TEXT,
          confidence TEXT,
          horizon TEXT,
          outcome_state TEXT DEFAULT 'open',
          outcome_json TEXT,
          last_evaluated_at TEXT,
          source TEXT,
          payload_json TEXT NOT NULL
        );
        CREATE TABLE IF NOT EXISTS entities (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          stable_id TEXT UNIQUE,
          first_seen TEXT NOT NULL,
          last_seen TEXT NOT NULL,
          name TEXT NOT NULL,
          kind TEXT NOT NULL,
          count INTEGER DEFAULT 1,
          UNIQUE(name, kind)
        );
        CREATE TABLE IF NOT EXISTS events (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          stable_id TEXT NOT NULL UNIQUE,
          first_seen TEXT NOT NULL,
          last_seen TEXT NOT NULL,
          kind TEXT NOT NULL,
          name TEXT NOT NULL,
          region TEXT,
          severity TEXT,
          source TEXT,
          evidence_json TEXT NOT NULL,
          count INTEGER DEFAULT 1
        );
      `);
      this._migrate();
      this.available = true;
    } catch (err) {
      this.available = false;
      this.reason = err.message;
      if (!existsSync(this.dbPath)) {
        writeFileSync(this.dbPath, '');
      }
    }
    return this;
  }

  recordRun(data, delta) {
    if (!this.available || !this.db) return;
    const meta = data.meta || {};
    const timestamp = meta.timestamp || new Date().toISOString();
    this.db.prepare(`INSERT INTO runs (timestamp, sources_ok, sources_degraded, sources_failed, direction, summary_json)
      VALUES (?, ?, ?, ?, ?, ?)`).run(
      timestamp,
      meta.sourcesOk || 0,
      meta.sourcesDegraded || 0,
      meta.sourcesFailed || 0,
      delta?.summary?.direction || null,
      JSON.stringify({ meta, delta: delta?.summary || null }),
    );
    this._recordEntities(data, timestamp);
    this._recordEvents(data, delta, timestamp);
    this.evaluatePredictions(data, timestamp);
    this._recordPredictions(data, timestamp);
  }

  status() {
    return { available: this.available, path: this.dbPath, reason: this.reason };
  }

  queryMemory({ q = '', limit = 25 } = {}) {
    if (!this.available || !this.db) return { available: false, reason: this.reason, results: [] };
    const safeLimit = Math.max(1, Math.min(100, Number(limit) || 25));
    const term = String(q || '').trim();
    const like = `%${term}%`;
    const where = term
      ? 'WHERE name LIKE ? OR region LIKE ? OR source LIKE ? OR kind LIKE ?'
      : '';
    const params = term ? [like, like, like, like, safeLimit] : [safeLimit];
    const events = this.db.prepare(`
      SELECT stable_id, first_seen, last_seen, kind, name, region, severity, source, count, evidence_json
      FROM events
      ${where}
      ORDER BY last_seen DESC
      LIMIT ?
    `).all(...params).map(row => ({ ...row, evidence: parseJson(row.evidence_json, {}) }));
    return { available: true, q: term, results: events };
  }

  listPredictions({ state = null, limit = 25 } = {}) {
    if (!this.available || !this.db) return { available: false, reason: this.reason, predictions: [] };
    const safeLimit = Math.max(1, Math.min(100, Number(limit) || 25));
    const normalizedState = state && PREDICTION_STATES.has(String(state)) ? String(state) : null;
    const rows = normalizedState
      ? this.db.prepare(`SELECT * FROM predictions WHERE outcome_state = ? ORDER BY created_at DESC LIMIT ?`).all(normalizedState, safeLimit)
      : this.db.prepare(`SELECT * FROM predictions ORDER BY created_at DESC LIMIT ?`).all(safeLimit);
    return {
      available: true,
      predictions: rows.map(row => ({
        stable_id: row.stable_id,
        created_at: row.created_at,
        updated_at: row.updated_at,
        title: row.title,
        type: row.type,
        hypothesis: row.hypothesis,
        confidence: row.confidence,
        horizon: row.horizon,
        outcome_state: row.outcome_state,
        last_evaluated_at: row.last_evaluated_at,
        source: row.source,
        evidence: parseJson(row.evidence_json, []),
        outcome: parseJson(row.outcome_json, null),
      })),
    };
  }

  evaluatePredictions(data, timestamp = new Date().toISOString()) {
    if (!this.available || !this.db) return;
    const rows = this.db.prepare(`
      SELECT id, created_at, title, type, horizon, outcome_state, payload_json
      FROM predictions
      WHERE outcome_state IN ('open', 'monitoring')
      ORDER BY created_at ASC
      LIMIT 200
    `).all();
    for (const row of rows) {
      const payload = parseJson(row.payload_json, {});
      const evaluation = evaluatePredictionAgainstSweep(row, payload, data, timestamp);
      this.db.prepare(`UPDATE predictions
        SET outcome_state = ?, outcome_json = ?, last_evaluated_at = ?, updated_at = ?
        WHERE id = ?`).run(
        evaluation.state,
        JSON.stringify(evaluation),
        timestamp,
        timestamp,
        row.id,
      );
    }
  }

  _migrate() {
    const columns = {
      predictions: [
        ['stable_id', 'TEXT'],
        ['updated_at', 'TEXT'],
        ['hypothesis', 'TEXT'],
        ['evidence_json', 'TEXT'],
        ['horizon', 'TEXT'],
        ['outcome_state', "TEXT DEFAULT 'open'"],
        ['outcome_json', 'TEXT'],
        ['last_evaluated_at', 'TEXT'],
      ],
      entities: [
        ['stable_id', 'TEXT'],
      ],
    };
    for (const [table, defs] of Object.entries(columns)) {
      for (const [name, type] of defs) {
        try { this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`); } catch { }
      }
    }
    try { this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_predictions_stable_id ON predictions(stable_id)`); } catch { }
    try { this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_entities_stable_id ON entities(stable_id)`); } catch { }
  }

  _recordPredictions(data, timestamp) {
    for (const idea of data.ideas || []) {
      const title = idea.title || 'Untitled idea';
      const predictionId = stableId('prediction', title, idea.type || '', idea.ticker || '', idea.horizon || '');
      const evidence = Array.isArray(idea.signals) ? idea.signals : [];
      this.db.prepare(`INSERT INTO predictions (
          stable_id, created_at, updated_at, title, type, hypothesis, evidence_json, confidence,
          horizon, outcome_state, source, payload_json
        )
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?)
        ON CONFLICT(stable_id) DO UPDATE SET
          updated_at=excluded.updated_at,
          confidence=excluded.confidence,
          evidence_json=excluded.evidence_json,
          payload_json=excluded.payload_json`).run(
        predictionId,
        timestamp,
        timestamp,
        title,
        idea.type || null,
        idea.rationale || idea.text || title,
        JSON.stringify(evidence),
        idea.confidence || null,
        idea.horizon || null,
        idea.source || data.ideasSource || null,
        JSON.stringify(idea),
      );
    }
  }

  _recordEntities(data, timestamp) {
    const names = [];
    for (const item of data.acled?.deadliestEvents || []) {
      if (item.country) names.push([item.country, 'country']);
      if (item.location) names.push([item.location, 'location']);
    }
    for (const item of data.news || []) {
      if (item.region) names.push([item.region, 'region']);
    }
    for (const [name, kind] of names.slice(0, 200)) {
      const cleanName = String(name).slice(0, 160);
      this.db.prepare(`INSERT INTO entities (stable_id, first_seen, last_seen, name, kind, count)
        VALUES (?, ?, ?, ?, ?, 1)
        ON CONFLICT(name, kind) DO UPDATE SET last_seen=excluded.last_seen, count=count+1`).run(
        stableId('entity', kind, cleanName),
        timestamp,
        timestamp,
        cleanName,
        kind,
      );
    }
  }

  _recordEvents(data, delta, timestamp) {
    const events = extractEvents(data, delta);
    for (const event of events.slice(0, 300)) {
      this.db.prepare(`INSERT INTO events (
          stable_id, first_seen, last_seen, kind, name, region, severity, source, evidence_json, count
        )
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
        ON CONFLICT(stable_id) DO UPDATE SET
          last_seen=excluded.last_seen,
          severity=COALESCE(excluded.severity, severity),
          evidence_json=excluded.evidence_json,
          count=count+1`).run(
        event.stable_id,
        timestamp,
        timestamp,
        event.kind,
        event.name,
        event.region || null,
        event.severity || null,
        event.source || null,
        JSON.stringify(event.evidence || {}),
      );
    }
  }
}

function stableId(...parts) {
  const input = parts.map(part => String(part || '').trim().toLowerCase()).join('|');
  return createHash('sha256').update(input).digest('hex').slice(0, 24);
}

function parseJson(value, fallback) {
  try { return value ? JSON.parse(value) : fallback; } catch { return fallback; }
}

function extractEvents(data, delta) {
  const events = [];
  const push = ({ kind, name, region, severity, source, evidence }) => {
    if (!kind || !name) return;
    events.push({
      stable_id: stableId('event', kind, name, region || source || ''),
      kind,
      name: String(name).slice(0, 240),
      region: region ? String(region).slice(0, 120) : null,
      severity: severity || null,
      source: source || null,
      evidence: evidence || {},
    });
  };

  for (const item of data.acled?.deadliestEvents || []) {
    push({
      kind: 'conflict',
      name: item.event_type || item.sub_event_type || item.location || item.country,
      region: item.country || item.location,
      severity: Number(item.fatalities || 0) > 0 ? 'high' : 'medium',
      source: 'ACLED',
      evidence: item,
    });
  }
  for (const item of data.tg?.urgent || []) {
    push({
      kind: 'osint',
      name: (item.text || '').slice(0, 120),
      region: item.region || 'OSINT',
      severity: 'high',
      source: item.channel || item.chat || 'telegram',
      evidence: item,
    });
  }
  for (const item of data.newsFeed || data.news || []) {
    if (!item.urgent) continue;
    push({
      kind: 'news',
      name: item.headline || item.title,
      region: item.region,
      severity: 'medium',
      source: item.source,
      evidence: item,
    });
  }
  for (const signal of delta?.signals?.new || []) {
    push({
      kind: 'delta',
      name: signal.label || signal.reason || signal.key,
      region: signal.region,
      severity: signal.severity || 'medium',
      source: 'delta',
      evidence: signal,
    });
  }
  return events;
}

function evaluatePredictionAgainstSweep(row, payload, data, timestamp) {
  const terms = [
    row.title,
    payload.ticker,
    ...(Array.isArray(payload.signals) ? payload.signals : []),
  ].filter(Boolean).map(v => String(v).toLowerCase());
  const evidenceText = [
    ...(data.tSignals || []),
    ...(data.newsFeed || []).slice(0, 40).map(n => `${n.source || ''} ${n.headline || n.title || ''}`),
    ...(data.tg?.urgent || []).slice(0, 20).map(p => p.text || ''),
  ].join('\n').toLowerCase();
  const matched = terms.filter(term => term.length >= 4 && evidenceText.includes(term.slice(0, 60)));
  const expired = predictionExpired(row.created_at, row.horizon, timestamp);
  const state = matched.length
    ? 'observed'
    : expired
      ? 'expired_unverified'
      : 'monitoring';
  return {
    state,
    evaluated_at: timestamp,
    matched_terms: matched.slice(0, 10),
    expired,
    reason: matched.length
      ? 'Current sweep contains matching evidence terms.'
      : expired
        ? 'Prediction horizon elapsed without matching evidence.'
        : 'Prediction remains open for future sweeps.',
  };
}

function predictionExpired(createdAt, horizon, nowIso) {
  const created = new Date(createdAt).getTime();
  const now = new Date(nowIso).getTime();
  if (!Number.isFinite(created) || !Number.isFinite(now)) return false;
  const text = String(horizon || '').toLowerCase();
  const days = text.includes('intraday') ? 1
    : text.includes('day') ? 7
      : text.includes('week') ? 45
        : text.includes('month') ? 180
          : text.includes('strategic') ? 365
            : 30;
  return now - created > days * 24 * 60 * 60 * 1000;
}
