/**
 * Session Context Manager
 *
 * Persists session context to Kuzu graph database.
 */

import crypto from 'crypto';
import type { KuzuConnection } from '../graph/connection.js';
import type { ContextItem, SessionContext } from './types.js';
import { createLogger, type Logger } from '../utils/logger.js';

/**
 * Schema DDL for context tables
 */
const CONTEXT_SCHEMA = `
CREATE NODE TABLE IF NOT EXISTS Session (
  session_id STRING,
  terminal_id STRING,
  created_at TIMESTAMP,
  last_active TIMESTAMP,
  PRIMARY KEY (session_id)
);

CREATE NODE TABLE IF NOT EXISTS ContextItem (
  id STRING,
  qualified_name STRING,
  entity_type STRING,
  description STRING,
  file_path STRING,
  score DOUBLE,
  touch_count INT64,
  last_touched TIMESTAMP,
  neighbors STRING[],
  PRIMARY KEY (id)
);

CREATE REL TABLE IF NOT EXISTS SESSION_HAS_CONTEXT (
  FROM Session TO ContextItem
);
`;

/**
 * Session Context Manager
 *
 * Manages session context persistence in Kuzu:
 * - Create/retrieve sessions by terminal ID
 * - Add/update context items
 * - Apply score decay
 */
export class SessionContextManager {
  private conn: KuzuConnection;
  private logger: Logger;
  private initialized = false;

  constructor(conn: KuzuConnection, logger?: Logger) {
    this.conn = conn;
    this.logger = logger ?? createLogger({ name: 'session-manager' });
  }

  /**
   * Initialize schema if needed
   */
  async initialize(): Promise<void> {
    if (this.initialized) return;

    try {
      // Execute schema DDL
      for (const stmt of CONTEXT_SCHEMA.split(';').filter((s) => s.trim())) {
        await this.conn.execute(stmt.trim());
      }
      this.initialized = true;
      this.logger.debug('Context schema initialized');
    } catch (error) {
      this.logger.error({ error }, 'Failed to initialize context schema');
      throw error;
    }
  }

  /**
   * Get or create session for a terminal
   */
  async getOrCreateSession(terminalId: string): Promise<SessionContext> {
    await this.initialize();

    // Try to find existing session
    const result = await this.conn.query(`
      MATCH (s:Session {terminal_id: $terminalId})
      RETURN s.session_id AS sessionId,
             s.created_at AS createdAt,
             s.last_active AS lastActive
      ORDER BY s.last_active DESC
      LIMIT 1
    `, { terminalId });

    const existing = result.single();
    if (existing) {
      return {
        sessionId: existing.sessionId as string,
        terminalId,
        items: await this.getSessionItems(existing.sessionId as string),
        createdAt: new Date(existing.createdAt as string),
        lastActive: new Date(existing.lastActive as string),
      };
    }

    // Create new session
    const sessionId = crypto.randomUUID();
    const now = new Date();

    await this.conn.execute(`
      CREATE (s:Session {
        session_id: $sessionId,
        terminal_id: $terminalId,
        created_at: $now,
        last_active: $now
      })
    `, { sessionId, terminalId, now: now.toISOString() });

    this.logger.debug({ sessionId, terminalId }, 'Created new session');

    return {
      sessionId,
      terminalId,
      items: [],
      createdAt: now,
      lastActive: now,
    };
  }

  /**
   * Get items for a session
   */
  async getSessionItems(sessionId: string): Promise<ContextItem[]> {
    const result = await this.conn.query(`
      MATCH (s:Session {session_id: $sessionId})-[:SESSION_HAS_CONTEXT]->(c:ContextItem)
      RETURN c.qualified_name AS qualifiedName,
             c.entity_type AS entityType,
             c.description AS description,
             c.file_path AS filePath,
             c.score AS score,
             c.touch_count AS touchCount,
             c.last_touched AS lastTouched,
             c.neighbors AS neighbors
      ORDER BY c.score DESC
    `, { sessionId });

    return result.all().map((row) => ({
      qualifiedName: row.qualifiedName as string,
      entityType: row.entityType as 'Function' | 'Class' | 'File',
      description: row.description as string | undefined,
      filePath: row.filePath as string | undefined,
      score: row.score as number,
      touchCount: row.touchCount as number,
      lastTouched: new Date(row.lastTouched as string),
      neighbors: (row.neighbors as string[]) ?? [],
    }));
  }

  /**
   * Add or update context items in a session
   */
  async addContextItems(
    sessionId: string,
    items: ContextItem[],
    decayFactor: number = 0.8
  ): Promise<void> {
    if (items.length === 0) return;

    await this.initialize();

    // Apply decay to existing items
    await this.decayExisting(sessionId, decayFactor);

    // Upsert new items
    for (const item of items) {
      const itemId = `${sessionId}:${item.qualifiedName}`;

      // Check if exists
      const existing = await this.conn.query(`
        MATCH (c:ContextItem {id: $itemId})
        RETURN c.touch_count AS touchCount
      `, { itemId });

      const existingRow = existing.single();

      if (existingRow) {
        // Update existing: increment touch count, reset score
        await this.conn.execute(`
          MATCH (c:ContextItem {id: $itemId})
          SET c.score = $score,
              c.touch_count = $touchCount,
              c.last_touched = $lastTouched,
              c.neighbors = $neighbors,
              c.description = $description
        `, {
          itemId,
          score: item.score,
          touchCount: (existingRow.touchCount as number) + 1,
          lastTouched: item.lastTouched.toISOString(),
          neighbors: item.neighbors,
          description: item.description ?? '',
        });
      } else {
        // Create new item and link to session
        await this.conn.execute(`
          CREATE (c:ContextItem {
            id: $itemId,
            qualified_name: $qualifiedName,
            entity_type: $entityType,
            description: $description,
            file_path: $filePath,
            score: $score,
            touch_count: $touchCount,
            last_touched: $lastTouched,
            neighbors: $neighbors
          })
        `, {
          itemId,
          qualifiedName: item.qualifiedName,
          entityType: item.entityType,
          description: item.description ?? '',
          filePath: item.filePath ?? '',
          score: item.score,
          touchCount: item.touchCount,
          lastTouched: item.lastTouched.toISOString(),
          neighbors: item.neighbors,
        });

        // Create relationship
        await this.conn.execute(`
          MATCH (s:Session {session_id: $sessionId}), (c:ContextItem {id: $itemId})
          CREATE (s)-[:SESSION_HAS_CONTEXT]->(c)
        `, { sessionId, itemId });
      }
    }

    // Update session last_active
    await this.conn.execute(`
      MATCH (s:Session {session_id: $sessionId})
      SET s.last_active = $now
    `, { sessionId, now: new Date().toISOString() });

    this.logger.debug({ sessionId, itemCount: items.length }, 'Added context items');
  }

  /**
   * Apply decay to existing items
   */
  private async decayExisting(sessionId: string, decayFactor: number): Promise<void> {
    await this.conn.execute(`
      MATCH (s:Session {session_id: $sessionId})-[:SESSION_HAS_CONTEXT]->(c:ContextItem)
      SET c.score = c.score * $decayFactor
    `, { sessionId, decayFactor });
  }

  /**
   * Remove items below score threshold
   */
  async pruneItems(sessionId: string, minScore: number): Promise<number> {
    // Get items to delete
    const toDelete = await this.conn.query(`
      MATCH (s:Session {session_id: $sessionId})-[:SESSION_HAS_CONTEXT]->(c:ContextItem)
      WHERE c.score < $minScore
      RETURN c.id AS itemId
    `, { sessionId, minScore });

    const ids = toDelete.all().map((r) => r.itemId as string);

    if (ids.length === 0) return 0;

    // Delete items
    for (const itemId of ids) {
      await this.conn.execute(`
        MATCH (c:ContextItem {id: $itemId})
        DETACH DELETE c
      `, { itemId });
    }

    this.logger.debug({ sessionId, pruned: ids.length }, 'Pruned low-score items');
    return ids.length;
  }

  /**
   * Clear all items in a session
   */
  async clearSession(sessionId: string): Promise<void> {
    await this.conn.execute(`
      MATCH (s:Session {session_id: $sessionId})-[:SESSION_HAS_CONTEXT]->(c:ContextItem)
      DETACH DELETE c
    `, { sessionId });

    this.logger.debug({ sessionId }, 'Cleared session context');
  }

  /**
   * Delete a session and all its items
   */
  async deleteSession(sessionId: string): Promise<void> {
    await this.clearSession(sessionId);
    await this.conn.execute(`
      MATCH (s:Session {session_id: $sessionId})
      DELETE s
    `, { sessionId });

    this.logger.debug({ sessionId }, 'Deleted session');
  }
}

/**
 * Get terminal ID from environment or generate one
 */
export function getTerminalId(): string {
  return process.env.EMDASH_TERMINAL_ID ?? crypto.randomUUID();
}
