/**
 * Kuzu database connection management
 */

import Database from 'kuzu';
import type { Connection, QueryResult } from 'kuzu';
import { mkdir, access, writeFile, readFile, rm } from 'fs/promises';
import { join, dirname } from 'path';
import { createLogger, type Logger } from '../utils/logger.js';
import { SchemaManager } from './schema.js';

const LOCK_FILE = 'kuzu.lock';
const LOCK_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes

/**
 * Connection options
 */
export interface ConnectionOptions {
  /** Path to the database directory */
  databasePath: string;
  /** Read-only mode */
  readOnly?: boolean;
  /** Buffer pool size in bytes */
  bufferPoolSize?: number;
  /** Maximum number of threads */
  maxThreads?: number;
  /** Logger instance */
  logger?: Logger;
}

/**
 * Query result wrapper for Neo4j-compatible API
 */
export class QueryResultWrapper {
  private results: Record<string, unknown>[];

  constructor(queryResult: QueryResult) {
    this.results = [];

    // Convert Kuzu result to array of records
    while (queryResult.hasNext()) {
      const row = queryResult.getNext();
      const record: Record<string, unknown> = {};

      // Get column names and values
      const numColumns = queryResult.getNumColumns();
      for (let i = 0; i < numColumns; i++) {
        const columnName = queryResult.getColumnNames()[i];
        record[columnName] = row[i];
      }

      this.results.push(record);
    }
  }

  /**
   * Get single result (first row)
   */
  single(): Record<string, unknown> | null {
    return this.results[0] ?? null;
  }

  /**
   * Get all results
   */
  all(): Record<string, unknown>[] {
    return this.results;
  }

  /**
   * Get result count
   */
  get length(): number {
    return this.results.length;
  }

  /**
   * Iterate over results
   */
  [Symbol.iterator](): Iterator<Record<string, unknown>> {
    return this.results[Symbol.iterator]();
  }
}

/**
 * Kuzu Connection Manager
 *
 * Manages database connections with:
 * - Lazy initialization
 * - Write lock coordination
 * - Query execution helpers
 * - Neo4j-compatible API
 */
export class KuzuConnection {
  private db: Database | null = null;
  private conn: Connection | null = null;
  private options: ConnectionOptions;
  private logger: Logger;
  private schemaManager: SchemaManager | null = null;

  constructor(options: ConnectionOptions) {
    this.options = options;
    this.logger = options.logger ?? createLogger({ name: 'kuzu-connection' });
  }

  /**
   * Connect to the database
   */
  async connect(): Promise<Connection> {
    if (this.conn) {
      return this.conn;
    }

    const { databasePath, readOnly, bufferPoolSize, maxThreads } = this.options;

    // Ensure directory exists
    await mkdir(dirname(databasePath), { recursive: true });

    this.logger.debug({ databasePath, readOnly }, 'Connecting to Kuzu database');

    // Create database instance
    this.db = new Database(databasePath, bufferPoolSize ?? 256 * 1024 * 1024);

    // Set max threads if specified
    if (maxThreads) {
      this.db.setMaxNumThreads(maxThreads);
    }

    // Create connection
    this.conn = this.db.getConnection();

    this.logger.info({ databasePath }, 'Connected to Kuzu database');

    return this.conn;
  }

  /**
   * Get the connection (connects if needed)
   */
  async getConnection(): Promise<Connection> {
    if (!this.conn) {
      await this.connect();
    }
    return this.conn!;
  }

  /**
   * Get the schema manager
   */
  async getSchemaManager(): Promise<SchemaManager> {
    if (!this.schemaManager) {
      const conn = await this.getConnection();
      this.schemaManager = new SchemaManager(conn, this.logger);
    }
    return this.schemaManager;
  }

  /**
   * Initialize schema
   */
  async initializeSchema(): Promise<void> {
    const schemaManager = await this.getSchemaManager();
    await schemaManager.initialize();
  }

  /**
   * Execute a read query
   */
  async query(cypher: string, params?: Record<string, unknown>): Promise<QueryResultWrapper> {
    const conn = await this.getConnection();

    this.logger.debug({ cypher, params }, 'Executing query');

    try {
      const result = await conn.query(cypher, params);
      return new QueryResultWrapper(result);
    } catch (err) {
      this.logger.error({ cypher, params, error: err }, 'Query failed');
      throw err;
    }
  }

  /**
   * Execute a write query
   */
  async execute(cypher: string, params?: Record<string, unknown>): Promise<void> {
    const conn = await this.getConnection();

    this.logger.debug({ cypher, params }, 'Executing write');

    try {
      await conn.query(cypher, params);
    } catch (err) {
      this.logger.error({ cypher, params, error: err }, 'Write failed');
      throw err;
    }
  }

  /**
   * Verify connection is working
   */
  async verify(): Promise<boolean> {
    try {
      const result = await this.query('RETURN 1 AS test');
      return result.single()?.test === 1;
    } catch {
      return false;
    }
  }

  /**
   * Get database info
   */
  async getDatabaseInfo(): Promise<{
    nodeCount: Record<string, number>;
    relCount: Record<string, number>;
  }> {
    const nodeCount: Record<string, number> = {};
    const relCount: Record<string, number> = {};

    // Count nodes
    for (const table of ['File', 'Class', 'Function', 'Module']) {
      try {
        const result = await this.query(`MATCH (n:${table}) RETURN count(n) AS count`);
        nodeCount[table] = (result.single()?.count as number) ?? 0;
      } catch {
        nodeCount[table] = 0;
      }
    }

    // Count relationships
    for (const table of ['CONTAINS_CLASS', 'CONTAINS_FUNCTION', 'HAS_METHOD', 'INHERITS_FROM', 'CALLS', 'IMPORTS']) {
      try {
        const result = await this.query(`MATCH ()-[r:${table}]->() RETURN count(r) AS count`);
        relCount[table] = (result.single()?.count as number) ?? 0;
      } catch {
        relCount[table] = 0;
      }
    }

    return { nodeCount, relCount };
  }

  /**
   * Close the connection
   */
  async close(): Promise<void> {
    if (this.conn) {
      // Kuzu connections are automatically cleaned up
      this.conn = null;
    }
    if (this.db) {
      this.db.close();
      this.db = null;
    }
    this.schemaManager = null;
    this.logger.debug('Connection closed');
  }

  /**
   * Acquire write lock
   */
  async acquireWriteLock(operation: string): Promise<boolean> {
    const lockPath = join(dirname(this.options.databasePath), LOCK_FILE);

    try {
      // Check if lock exists
      try {
        await access(lockPath);
        const content = await readFile(lockPath, 'utf-8');
        const lock = JSON.parse(content);

        // Check if lock is stale
        const lockAge = Date.now() - lock.timestamp;
        if (lockAge < LOCK_TIMEOUT_MS) {
          this.logger.warn({ existingLock: lock }, 'Write lock held by another process');
          return false;
        }

        // Lock is stale, remove it
        this.logger.warn({ existingLock: lock }, 'Removing stale lock');
        await rm(lockPath);
      } catch {
        // Lock doesn't exist, proceed
      }

      // Create lock
      const lockData = {
        operation,
        timestamp: Date.now(),
        pid: process.pid,
      };
      await writeFile(lockPath, JSON.stringify(lockData), 'utf-8');

      this.logger.debug({ operation }, 'Write lock acquired');
      return true;
    } catch (err) {
      this.logger.error({ error: err }, 'Failed to acquire write lock');
      return false;
    }
  }

  /**
   * Release write lock
   */
  async releaseWriteLock(): Promise<void> {
    const lockPath = join(dirname(this.options.databasePath), LOCK_FILE);

    try {
      await rm(lockPath);
      this.logger.debug('Write lock released');
    } catch {
      // Lock might not exist
    }
  }

  /**
   * Execute with write lock
   */
  async withWriteLock<T>(operation: string, fn: () => Promise<T>): Promise<T> {
    const acquired = await this.acquireWriteLock(operation);
    if (!acquired) {
      throw new Error(`Could not acquire write lock for: ${operation}`);
    }

    try {
      return await fn();
    } finally {
      await this.releaseWriteLock();
    }
  }
}

// Global connection instance
let globalConnection: KuzuConnection | null = null;

/**
 * Get or create global connection
 */
export function getConnection(options?: ConnectionOptions): KuzuConnection {
  if (!globalConnection && options) {
    globalConnection = new KuzuConnection(options);
  }
  if (!globalConnection) {
    throw new Error('Connection not initialized. Call getConnection with options first.');
  }
  return globalConnection;
}

/**
 * Configure connection for a repository
 */
export function configureForRepo(repoRoot: string, options?: Partial<ConnectionOptions>): KuzuConnection {
  const databasePath = join(repoRoot, '.emdash', 'kuzu_db');

  globalConnection = new KuzuConnection({
    databasePath,
    ...options,
  });

  return globalConnection;
}

/**
 * Close global connection
 */
export async function closeConnection(): Promise<void> {
  if (globalConnection) {
    await globalConnection.close();
    globalConnection = null;
  }
}
