/**
 * Graph writer for batch operations
 *
 * Provides efficient batch writing of entities to the graph database.
 */

import type { KuzuConnection } from './connection.js';
import type {
  FileEntity,
  ClassEntity,
  FunctionEntity,
  ModuleEntity,
  ImportStatement,
} from './types.js';
import { createLogger, type Logger } from '../utils/logger.js';

const DEFAULT_BATCH_SIZE = 1000;

/**
 * Graph Writer for Layer A - Code Structure
 *
 * Handles batch writing of code entities to the graph.
 */
export class GraphWriter {
  private conn: KuzuConnection;
  private batchSize: number;
  private logger: Logger;

  constructor(conn: KuzuConnection, options?: { batchSize?: number; logger?: Logger }) {
    this.conn = conn;
    this.batchSize = options?.batchSize ?? DEFAULT_BATCH_SIZE;
    this.logger = options?.logger ?? createLogger({ name: 'graph-writer' });
  }

  /**
   * Write file nodes
   */
  async writeFiles(files: FileEntity[]): Promise<number> {
    if (files.length === 0) return 0;

    this.logger.debug({ count: files.length }, 'Writing file nodes');
    let written = 0;

    for (const batch of this.batchIterator(files)) {
      const rows = batch.map((f) => ({
        path: f.path,
        name: f.name,
        extension: f.extension,
        size_bytes: f.sizeBytes,
        lines_of_code: f.linesOfCode,
        hash: f.hash,
        last_modified: f.lastModified.toISOString(),
      }));

      await this.conn.execute(
        `
        UNWIND $rows AS row
        MERGE (f:File {path: row.path})
        SET f.name = row.name,
            f.extension = row.extension,
            f.size_bytes = row.size_bytes,
            f.lines_of_code = row.lines_of_code,
            f.hash = row.hash,
            f.last_modified = row.last_modified
        `,
        { rows }
      );

      written += batch.length;
    }

    this.logger.info({ written }, 'File nodes written');
    return written;
  }

  /**
   * Write class nodes
   */
  async writeClasses(classes: ClassEntity[]): Promise<number> {
    if (classes.length === 0) return 0;

    this.logger.debug({ count: classes.length }, 'Writing class nodes');
    let written = 0;

    for (const batch of this.batchIterator(classes)) {
      const rows = batch.map((c) => ({
        qualified_name: c.qualifiedName,
        name: c.name,
        file_path: c.filePath,
        line_start: c.lineStart,
        line_end: c.lineEnd,
        docstring: c.docstring ?? '',
        is_abstract: c.isAbstract,
        decorators: c.decorators,
        base_classes: c.baseClasses,
        embedding: c.embedding ?? [],
      }));

      // Write nodes
      await this.conn.execute(
        `
        UNWIND $rows AS row
        MERGE (c:Class {qualified_name: row.qualified_name})
        SET c.name = row.name,
            c.file_path = row.file_path,
            c.line_start = row.line_start,
            c.line_end = row.line_end,
            c.docstring = row.docstring,
            c.is_abstract = row.is_abstract,
            c.decorators = row.decorators,
            c.base_classes = row.base_classes,
            c.embedding = row.embedding
        `,
        { rows }
      );

      // Write CONTAINS_CLASS relationships
      await this.conn.execute(
        `
        UNWIND $rows AS row
        MATCH (f:File {path: row.file_path})
        MATCH (c:Class {qualified_name: row.qualified_name})
        MERGE (f)-[r:CONTAINS_CLASS]->(c)
        SET r.line_start = row.line_start
        `,
        { rows }
      );

      written += batch.length;
    }

    this.logger.info({ written }, 'Class nodes written');
    return written;
  }

  /**
   * Write function nodes
   */
  async writeFunctions(functions: FunctionEntity[]): Promise<number> {
    if (functions.length === 0) return 0;

    this.logger.debug({ count: functions.length }, 'Writing function nodes');
    let written = 0;

    for (const batch of this.batchIterator(functions)) {
      const rows = batch.map((f) => ({
        qualified_name: f.qualifiedName,
        name: f.name,
        file_path: f.filePath,
        line_start: f.lineStart,
        line_end: f.lineEnd,
        docstring: f.docstring ?? '',
        parameters: f.parameters,
        return_annotation: f.returnAnnotation ?? '',
        is_async: f.isAsync,
        is_method: f.isMethod,
        is_static: f.isStatic,
        decorators: f.decorators,
        calls: f.calls,
        parent_class: f.parentClass ?? '',
        cyclomatic_complexity: f.cyclomaticComplexity ?? 0,
        embedding: f.embedding ?? [],
      }));

      // Write nodes
      await this.conn.execute(
        `
        UNWIND $rows AS row
        MERGE (fn:Function {qualified_name: row.qualified_name})
        SET fn.name = row.name,
            fn.file_path = row.file_path,
            fn.line_start = row.line_start,
            fn.line_end = row.line_end,
            fn.docstring = row.docstring,
            fn.parameters = row.parameters,
            fn.return_annotation = row.return_annotation,
            fn.is_async = row.is_async,
            fn.is_method = row.is_method,
            fn.is_static = row.is_static,
            fn.decorators = row.decorators,
            fn.calls = row.calls,
            fn.parent_class = row.parent_class,
            fn.cyclomatic_complexity = row.cyclomatic_complexity,
            fn.embedding = row.embedding
        `,
        { rows }
      );

      // Write CONTAINS_FUNCTION relationships
      await this.conn.execute(
        `
        UNWIND $rows AS row
        MATCH (f:File {path: row.file_path})
        MATCH (fn:Function {qualified_name: row.qualified_name})
        MERGE (f)-[r:CONTAINS_FUNCTION]->(fn)
        SET r.line_start = row.line_start
        `,
        { rows }
      );

      // Write HAS_METHOD relationships for methods
      const methodRows = rows.filter((r) => r.is_method && r.parent_class);
      if (methodRows.length > 0) {
        await this.conn.execute(
          `
          UNWIND $rows AS row
          MATCH (c:Class {qualified_name: row.parent_class})
          MATCH (fn:Function {qualified_name: row.qualified_name})
          MERGE (c)-[:HAS_METHOD]->(fn)
          `,
          { rows: methodRows }
        );
      }

      written += batch.length;
    }

    this.logger.info({ written }, 'Function nodes written');
    return written;
  }

  /**
   * Write module nodes
   */
  async writeModules(modules: ModuleEntity[]): Promise<number> {
    if (modules.length === 0) return 0;

    this.logger.debug({ count: modules.length }, 'Writing module nodes');
    let written = 0;

    for (const batch of this.batchIterator(modules)) {
      const rows = batch.map((m) => ({
        name: m.name,
        import_path: m.importPath,
        is_external: m.isExternal,
        package: m.package ?? '',
      }));

      await this.conn.execute(
        `
        UNWIND $rows AS row
        MERGE (m:Module {name: row.name})
        SET m.import_path = row.import_path,
            m.is_external = row.is_external,
            m.package = row.package
        `,
        { rows }
      );

      written += batch.length;
    }

    this.logger.info({ written }, 'Module nodes written');
    return written;
  }

  /**
   * Write import relationships
   */
  async writeImports(imports: ImportStatement[]): Promise<number> {
    if (imports.length === 0) return 0;

    this.logger.debug({ count: imports.length }, 'Writing import relationships');
    let written = 0;

    for (const batch of this.batchIterator(imports)) {
      const rows = batch.map((i) => ({
        file_path: i.filePath,
        module_name: i.moduleName,
        import_type: i.importType,
        line_number: i.lineNumber,
        alias: i.alias ?? '',
      }));

      await this.conn.execute(
        `
        UNWIND $rows AS row
        MATCH (f:File {path: row.file_path})
        MATCH (m:Module {name: row.module_name})
        MERGE (f)-[r:IMPORTS]->(m)
        SET r.import_type = row.import_type,
            r.line_number = row.line_number,
            r.alias = row.alias
        `,
        { rows }
      );

      written += batch.length;
    }

    this.logger.info({ written }, 'Import relationships written');
    return written;
  }

  /**
   * Write inheritance relationships
   */
  async writeInheritance(classes: ClassEntity[]): Promise<number> {
    const classesWithBases = classes.filter((c) => c.baseClasses.length > 0);
    if (classesWithBases.length === 0) return 0;

    this.logger.debug({ count: classesWithBases.length }, 'Writing inheritance relationships');
    let written = 0;

    for (const cls of classesWithBases) {
      for (const baseClass of cls.baseClasses) {
        try {
          await this.conn.execute(
            `
            MATCH (child:Class {qualified_name: $child_name})
            MATCH (parent:Class {qualified_name: $parent_name})
            MERGE (child)-[:INHERITS_FROM]->(parent)
            `,
            { child_name: cls.qualifiedName, parent_name: baseClass }
          );
          written++;
        } catch {
          // Parent class might not exist (external or not parsed)
          this.logger.debug(
            { child: cls.qualifiedName, parent: baseClass },
            'Could not create inheritance (parent not found)'
          );
        }
      }
    }

    this.logger.info({ written }, 'Inheritance relationships written');
    return written;
  }

  /**
   * Write call relationships
   */
  async writeCalls(functions: FunctionEntity[]): Promise<number> {
    const functionsWithCalls = functions.filter((f) => f.calls.length > 0);
    if (functionsWithCalls.length === 0) return 0;

    this.logger.debug({ count: functionsWithCalls.length }, 'Writing call relationships');
    let written = 0;

    for (const fn of functionsWithCalls) {
      for (const calledFn of fn.calls) {
        try {
          await this.conn.execute(
            `
            MATCH (caller:Function {qualified_name: $caller_name})
            MATCH (callee:Function {qualified_name: $callee_name})
            MERGE (caller)-[:CALLS]->(callee)
            `,
            { caller_name: fn.qualifiedName, callee_name: calledFn }
          );
          written++;
        } catch {
          // Called function might not exist
          this.logger.debug(
            { caller: fn.qualifiedName, callee: calledFn },
            'Could not create call (callee not found)'
          );
        }
      }
    }

    this.logger.info({ written }, 'Call relationships written');
    return written;
  }

  /**
   * Delete files and cascading entities
   */
  async deleteFiles(filePaths: string[]): Promise<number> {
    if (filePaths.length === 0) return 0;

    this.logger.info({ count: filePaths.length }, 'Deleting files');

    // Delete classes in these files
    await this.conn.execute(
      `
      UNWIND $paths AS path
      MATCH (c:Class {file_path: path})
      DETACH DELETE c
      `,
      { paths: filePaths }
    );

    // Delete functions in these files
    await this.conn.execute(
      `
      UNWIND $paths AS path
      MATCH (fn:Function {file_path: path})
      DETACH DELETE fn
      `,
      { paths: filePaths }
    );

    // Delete file nodes
    await this.conn.execute(
      `
      UNWIND $paths AS path
      MATCH (f:File {path: path})
      DETACH DELETE f
      `,
      { paths: filePaths }
    );

    return filePaths.length;
  }

  /**
   * Clear all data
   */
  async clearAll(): Promise<void> {
    this.logger.warn('Clearing all graph data');

    // Delete all relationships first
    await this.conn.execute('MATCH ()-[r]->() DELETE r');

    // Delete all nodes
    await this.conn.execute('MATCH (n) DELETE n');

    this.logger.info('All graph data cleared');
  }

  /**
   * Batch iterator helper
   */
  private *batchIterator<T>(items: T[]): Generator<T[]> {
    for (let i = 0; i < items.length; i += this.batchSize) {
      yield items.slice(i, i + this.batchSize);
    }
  }
}
