import { createHash } from 'crypto';
import path from 'path';
import { promises as fs } from 'fs';
import { logger } from '../utils/logger';
import { NodeSourceExtractor } from '../utils/node-source-extractor';
import { 
  EnhancedDocumentationFetcher,
  EnhancedNodeDocumentation,
  OperationInfo,
  ApiMethodMapping,
  CodeExample,
  TemplateInfo,
  RelatedResource
} from '../utils/enhanced-documentation-fetcher';
import { ExampleGenerator } from '../utils/example-generator';
import { DatabaseAdapter, createDatabaseAdapter } from '../database/database-adapter';

interface NodeInfo {
  nodeType: string;
  name: string;
  displayName: string;
  description: string;
  category?: string;
  subcategory?: string;
  icon?: string;
  sourceCode: string;
  credentialCode?: string;
  documentationMarkdown?: string;
  documentationUrl?: string;
  documentationTitle?: string;
  operations?: OperationInfo[];
  apiMethods?: ApiMethodMapping[];
  documentationExamples?: CodeExample[];
  templates?: TemplateInfo[];
  relatedResources?: RelatedResource[];
  requiredScopes?: string[];
  exampleWorkflow?: any;
  exampleParameters?: any;
  propertiesSchema?: any;
  packageName: string;
  version?: string;
  codexData?: any;
  aliases?: string[];
  hasCredentials: boolean;
  isTrigger: boolean;
  isWebhook: boolean;
}

interface SearchOptions {
  query?: string;
  nodeType?: string;
  packageName?: string;
  category?: string;
  hasCredentials?: boolean;
  isTrigger?: boolean;
  limit?: number;
}

export class NodeDocumentationService {
  private db: DatabaseAdapter | null = null;
  private extractor: NodeSourceExtractor;
  private docsFetcher: EnhancedDocumentationFetcher;
  private dbPath: string;
  private initialized: Promise<void>;
  
  constructor(dbPath?: string) {
    // Determine database path with multiple fallbacks for npx support
    this.dbPath = dbPath || process.env.NODE_DB_PATH || this.findDatabasePath();
    
    // Ensure directory exists
    const dbDir = path.dirname(this.dbPath);
    if (!require('fs').existsSync(dbDir)) {
      require('fs').mkdirSync(dbDir, { recursive: true });
    }
    
    this.extractor = new NodeSourceExtractor();
    this.docsFetcher = new EnhancedDocumentationFetcher();
    
    // Initialize database asynchronously
    this.initialized = this.initializeAsync();
  }
  
  private findDatabasePath(): string {
    const fs = require('fs');
    
    // Priority order for database locations:
    // 1. Local working directory (current behavior)
    const localPath = path.join(process.cwd(), 'data', 'nodes.db');
    if (fs.existsSync(localPath)) {
      return localPath;
    }
    
    // 2. Package installation directory (for npx)
    const packagePath = path.join(__dirname, '..', '..', 'data', 'nodes.db');
    if (fs.existsSync(packagePath)) {
      return packagePath;
    }
    
    // 3. Global npm modules directory (for global install)
    const globalPath = path.join(__dirname, '..', '..', '..', 'data', 'nodes.db');
    if (fs.existsSync(globalPath)) {
      return globalPath;
    }
    
    // 4. Default to local path (will be created if needed)
    return localPath;
  }
  
  private async initializeAsync(): Promise<void> {
    try {
      this.db = await createDatabaseAdapter(this.dbPath);
      
      // Initialize database with new schema
      this.initializeDatabase();
      
      logger.info('Node Documentation Service initialized');
    } catch (error) {
      logger.error('Failed to initialize database adapter', error);
      throw error;
    }
  }
  
  private async ensureInitialized(): Promise<void> {
    await this.initialized;
    if (!this.db) {
      throw new Error('Database not initialized');
    }
  }

  private initializeDatabase(): void {
    if (!this.db) throw new Error('Database not initialized');
    // Execute the schema directly
    const schema = `
-- Main nodes table with documentation and examples
CREATE TABLE IF NOT EXISTS nodes (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  node_type TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  display_name TEXT,
  description TEXT,
  category TEXT,
  subcategory TEXT,
  icon TEXT,
  
  -- Source code
  source_code TEXT NOT NULL,
  credential_code TEXT,
  code_hash TEXT NOT NULL,
  code_length INTEGER NOT NULL,
  
  -- Documentation
  documentation_markdown TEXT,
  documentation_url TEXT,
  documentation_title TEXT,
  
  -- Enhanced documentation fields (stored as JSON)
  operations TEXT,
  api_methods TEXT,
  documentation_examples TEXT,
  templates TEXT,
  related_resources TEXT,
  required_scopes TEXT,
  
  -- Example usage
  example_workflow TEXT,
  example_parameters TEXT,
  properties_schema TEXT,
  
  -- Metadata
  package_name TEXT NOT NULL,
  version TEXT,
  codex_data TEXT,
  aliases TEXT,
  
  -- Flags
  has_credentials INTEGER DEFAULT 0,
  is_trigger INTEGER DEFAULT 0,
  is_webhook INTEGER DEFAULT 0,
  
  -- Timestamps
  extracted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Indexes
CREATE INDEX IF NOT EXISTS idx_nodes_package_name ON nodes(package_name);
CREATE INDEX IF NOT EXISTS idx_nodes_category ON nodes(category);
CREATE INDEX IF NOT EXISTS idx_nodes_code_hash ON nodes(code_hash);
CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
CREATE INDEX IF NOT EXISTS idx_nodes_is_trigger ON nodes(is_trigger);

-- Full Text Search
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
  node_type,
  name,
  display_name,
  description,
  category,
  documentation_markdown,
  aliases,
  content=nodes,
  content_rowid=id
);

-- Triggers for FTS
CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes
BEGIN
  INSERT INTO nodes_fts(rowid, node_type, name, display_name, description, category, documentation_markdown, aliases)
  VALUES (new.id, new.node_type, new.name, new.display_name, new.description, new.category, new.documentation_markdown, new.aliases);
END;

CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes
BEGIN
  DELETE FROM nodes_fts WHERE rowid = old.id;
END;

CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes
BEGIN
  DELETE FROM nodes_fts WHERE rowid = old.id;
  INSERT INTO nodes_fts(rowid, node_type, name, display_name, description, category, documentation_markdown, aliases)
  VALUES (new.id, new.node_type, new.name, new.display_name, new.description, new.category, new.documentation_markdown, new.aliases);
END;

-- Documentation sources table
CREATE TABLE IF NOT EXISTS documentation_sources (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  source TEXT NOT NULL,
  commit_hash TEXT,
  fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Statistics table
CREATE TABLE IF NOT EXISTS extraction_stats (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  total_nodes INTEGER NOT NULL,
  nodes_with_docs INTEGER NOT NULL,
  nodes_with_examples INTEGER NOT NULL,
  total_code_size INTEGER NOT NULL,
  total_docs_size INTEGER NOT NULL,
  extraction_date DATETIME DEFAULT CURRENT_TIMESTAMP
);
    `;
    
    this.db!.exec(schema);
  }

  /**
   * Store complete node information including docs and examples
   */
  async storeNode(nodeInfo: NodeInfo): Promise<void> {
    await this.ensureInitialized();
    const hash = this.generateHash(nodeInfo.sourceCode);
    
    const stmt = this.db!.prepare(`
      INSERT OR REPLACE INTO nodes (
        node_type, name, display_name, description, category, subcategory, icon,
        source_code, credential_code, code_hash, code_length,
        documentation_markdown, documentation_url, documentation_title,
        operations, api_methods, documentation_examples, templates, related_resources, required_scopes,
        example_workflow, example_parameters, properties_schema,
        package_name, version, codex_data, aliases,
        has_credentials, is_trigger, is_webhook
      ) VALUES (
        @nodeType, @name, @displayName, @description, @category, @subcategory, @icon,
        @sourceCode, @credentialCode, @hash, @codeLength,
        @documentation, @documentationUrl, @documentationTitle,
        @operations, @apiMethods, @documentationExamples, @templates, @relatedResources, @requiredScopes,
        @exampleWorkflow, @exampleParameters, @propertiesSchema,
        @packageName, @version, @codexData, @aliases,
        @hasCredentials, @isTrigger, @isWebhook
      )
    `);

    stmt.run({
      nodeType: nodeInfo.nodeType,
      name: nodeInfo.name,
      displayName: nodeInfo.displayName || nodeInfo.name,
      description: nodeInfo.description || '',
      category: nodeInfo.category || 'Other',
      subcategory: nodeInfo.subcategory || null,
      icon: nodeInfo.icon || null,
      sourceCode: nodeInfo.sourceCode,
      credentialCode: nodeInfo.credentialCode || null,
      hash,
      codeLength: nodeInfo.sourceCode.length,
      documentation: nodeInfo.documentationMarkdown || null,
      documentationUrl: nodeInfo.documentationUrl || null,
      documentationTitle: nodeInfo.documentationTitle || null,
      operations: nodeInfo.operations ? JSON.stringify(nodeInfo.operations) : null,
      apiMethods: nodeInfo.apiMethods ? JSON.stringify(nodeInfo.apiMethods) : null,
      documentationExamples: nodeInfo.documentationExamples ? JSON.stringify(nodeInfo.documentationExamples) : null,
      templates: nodeInfo.templates ? JSON.stringify(nodeInfo.templates) : null,
      relatedResources: nodeInfo.relatedResources ? JSON.stringify(nodeInfo.relatedResources) : null,
      requiredScopes: nodeInfo.requiredScopes ? JSON.stringify(nodeInfo.requiredScopes) : null,
      exampleWorkflow: nodeInfo.exampleWorkflow ? JSON.stringify(nodeInfo.exampleWorkflow) : null,
      exampleParameters: nodeInfo.exampleParameters ? JSON.stringify(nodeInfo.exampleParameters) : null,
      propertiesSchema: nodeInfo.propertiesSchema ? JSON.stringify(nodeInfo.propertiesSchema) : null,
      packageName: nodeInfo.packageName,
      version: nodeInfo.version || null,
      codexData: nodeInfo.codexData ? JSON.stringify(nodeInfo.codexData) : null,
      aliases: nodeInfo.aliases ? JSON.stringify(nodeInfo.aliases) : null,
      hasCredentials: nodeInfo.hasCredentials ? 1 : 0,
      isTrigger: nodeInfo.isTrigger ? 1 : 0,
      isWebhook: nodeInfo.isWebhook ? 1 : 0
    });
  }

  /**
   * Get complete node information
   */
  async getNodeInfo(nodeType: string): Promise<NodeInfo | null> {
    await this.ensureInitialized();
    const stmt = this.db!.prepare(`
      SELECT * FROM nodes WHERE node_type = ? OR name = ? COLLATE NOCASE
    `);
    
    const row = stmt.get(nodeType, nodeType);
    if (!row) return null;
    
    return this.rowToNodeInfo(row);
  }

  /**
   * Search nodes with various filters
   */
  async searchNodes(options: SearchOptions): Promise<NodeInfo[]> {
    await this.ensureInitialized();
    let query = 'SELECT * FROM nodes WHERE 1=1';
    const params: any = {};
    
    if (options.query) {
      query += ` AND id IN (
        SELECT rowid FROM nodes_fts 
        WHERE nodes_fts MATCH @query
      )`;
      params.query = options.query;
    }
    
    if (options.nodeType) {
      query += ' AND node_type LIKE @nodeType';
      params.nodeType = `%${options.nodeType}%`;
    }
    
    if (options.packageName) {
      query += ' AND package_name = @packageName';
      params.packageName = options.packageName;
    }
    
    if (options.category) {
      query += ' AND category = @category';
      params.category = options.category;
    }
    
    if (options.hasCredentials !== undefined) {
      query += ' AND has_credentials = @hasCredentials';
      params.hasCredentials = options.hasCredentials ? 1 : 0;
    }
    
    if (options.isTrigger !== undefined) {
      query += ' AND is_trigger = @isTrigger';
      params.isTrigger = options.isTrigger ? 1 : 0;
    }
    
    query += ' ORDER BY name LIMIT @limit';
    params.limit = options.limit || 20;
    
    const stmt = this.db!.prepare(query);
    const rows = stmt.all(params);
    
    return rows.map(row => this.rowToNodeInfo(row));
  }

  /**
   * List all nodes
   */
  async listNodes(): Promise<NodeInfo[]> {
    await this.ensureInitialized();
    const stmt = this.db!.prepare('SELECT * FROM nodes ORDER BY name');
    const rows = stmt.all();
    return rows.map(row => this.rowToNodeInfo(row));
  }

  /**
   * Extract and store all nodes with documentation
   */
  async rebuildDatabase(): Promise<{
    total: number;
    successful: number;
    failed: number;
    errors: string[];
  }> {
    await this.ensureInitialized();
    logger.info('Starting complete database rebuild...');
    
    // Clear existing data
    this.db!.exec('DELETE FROM nodes');
    this.db!.exec('DELETE FROM extraction_stats');
    
    // Ensure documentation repository is available
    await this.docsFetcher.ensureDocsRepository();
    
    const stats = {
      total: 0,
      successful: 0,
      failed: 0,
      errors: [] as string[]
    };
    
    try {
      // Get all available nodes
      const availableNodes = await this.extractor.listAvailableNodes();
      stats.total = availableNodes.length;
      
      logger.info(`Found ${stats.total} nodes to process`);
      
      // Process nodes in batches
      const batchSize = 10;
      for (let i = 0; i < availableNodes.length; i += batchSize) {
        const batch = availableNodes.slice(i, i + batchSize);
        
        await Promise.all(batch.map(async (node) => {
          try {
            // Build node type from package name and node name
            const nodeType = `n8n-nodes-base.${node.name}`;
            
            // Extract source code
            const nodeData = await this.extractor.extractNodeSource(nodeType);
            if (!nodeData || !nodeData.sourceCode) {
              throw new Error('Failed to extract node source');
            }
            
            // Parse node definition to get metadata
            const nodeDefinition = this.parseNodeDefinition(nodeData.sourceCode);
            
            // Get enhanced documentation
            const enhancedDocs = await this.docsFetcher.getEnhancedNodeDocumentation(nodeType);
            
            // Generate example
            const example = ExampleGenerator.generateFromNodeDefinition(nodeDefinition);
            
            // Prepare node info with enhanced documentation
            const nodeInfo: NodeInfo = {
              nodeType: nodeType,
              name: node.name,
              displayName: nodeDefinition.displayName || node.displayName || node.name,
              description: nodeDefinition.description || node.description || '',
              category: nodeDefinition.category || 'Other',
              subcategory: nodeDefinition.subcategory,
              icon: nodeDefinition.icon,
              sourceCode: nodeData.sourceCode,
              credentialCode: nodeData.credentialCode,
              documentationMarkdown: enhancedDocs?.markdown,
              documentationUrl: enhancedDocs?.url,
              documentationTitle: enhancedDocs?.title,
              operations: enhancedDocs?.operations,
              apiMethods: enhancedDocs?.apiMethods,
              documentationExamples: enhancedDocs?.examples,
              templates: enhancedDocs?.templates,
              relatedResources: enhancedDocs?.relatedResources,
              requiredScopes: enhancedDocs?.requiredScopes,
              exampleWorkflow: example,
              exampleParameters: example.nodes[0]?.parameters,
              propertiesSchema: nodeDefinition.properties,
              packageName: nodeData.packageInfo?.name || 'n8n-nodes-base',
              version: nodeDefinition.version,
              codexData: nodeDefinition.codex,
              aliases: nodeDefinition.alias,
              hasCredentials: !!nodeData.credentialCode,
              isTrigger: node.name.toLowerCase().includes('trigger'),
              isWebhook: node.name.toLowerCase().includes('webhook')
            };
            
            // Store in database
            await this.storeNode(nodeInfo);
            
            stats.successful++;
            logger.debug(`Processed node: ${nodeType}`);
          } catch (error) {
            stats.failed++;
            const errorMsg = `Failed to process ${node.name}: ${error instanceof Error ? error.message : String(error)}`;
            stats.errors.push(errorMsg);
            logger.error(errorMsg);
          }
        }));
        
        logger.info(`Progress: ${Math.min(i + batchSize, availableNodes.length)}/${stats.total} nodes processed`);
      }
      
      // Store statistics
      this.storeStatistics(stats);
      
      logger.info(`Database rebuild complete: ${stats.successful} successful, ${stats.failed} failed`);
      
    } catch (error) {
      logger.error('Database rebuild failed:', error);
      throw error;
    }
    
    return stats;
  }

  /**
   * Parse node definition from source code
   */
  private parseNodeDefinition(sourceCode: string): any {
    const result: any = {
      displayName: '',
      description: '',
      properties: [],
      category: null,
      subcategory: null,
      icon: null,
      version: null,
      codex: null,
      alias: null
    };
    
    try {
      // Extract individual properties using specific patterns
      
      // Display name
      const displayNameMatch = sourceCode.match(/displayName\s*[:=]\s*['"`]([^'"`]+)['"`]/);
      if (displayNameMatch) {
        result.displayName = displayNameMatch[1];
      }
      
      // Description
      const descriptionMatch = sourceCode.match(/description\s*[:=]\s*['"`]([^'"`]+)['"`]/);
      if (descriptionMatch) {
        result.description = descriptionMatch[1];
      }
      
      // Icon
      const iconMatch = sourceCode.match(/icon\s*[:=]\s*['"`]([^'"`]+)['"`]/);
      if (iconMatch) {
        result.icon = iconMatch[1];
      }
      
      // Category/group
      const groupMatch = sourceCode.match(/group\s*[:=]\s*\[['"`]([^'"`]+)['"`]\]/);
      if (groupMatch) {
        result.category = groupMatch[1];
      }
      
      // Version
      const versionMatch = sourceCode.match(/version\s*[:=]\s*(\d+)/);
      if (versionMatch) {
        result.version = parseInt(versionMatch[1]);
      }
      
      // Subtitle
      const subtitleMatch = sourceCode.match(/subtitle\s*[:=]\s*['"`]([^'"`]+)['"`]/);
      if (subtitleMatch) {
        result.subtitle = subtitleMatch[1];
      }
      
      // Try to extract properties array
      const propsMatch = sourceCode.match(/properties\s*[:=]\s*(\[[\s\S]*?\])\s*[,}]/);
      if (propsMatch) {
        try {
          // This is complex to parse from minified code, so we'll skip for now
          result.properties = [];
        } catch (e) {
          // Ignore parsing errors
        }
      }
      
      // Check if it's a trigger node
      if (sourceCode.includes('implements.*ITrigger') || 
          sourceCode.includes('polling:.*true') ||
          sourceCode.includes('webhook:.*true') ||
          result.displayName.toLowerCase().includes('trigger')) {
        result.isTrigger = true;
      }
      
      // Check if it's a webhook node
      if (sourceCode.includes('webhooks:') || 
          sourceCode.includes('webhook:.*true') ||
          result.displayName.toLowerCase().includes('webhook')) {
        result.isWebhook = true;
      }
      
    } catch (error) {
      logger.debug('Error parsing node definition:', error);
    }
    
    return result;
  }

  /**
   * Convert database row to NodeInfo
   */
  private rowToNodeInfo(row: any): NodeInfo {
    return {
      nodeType: row.node_type,
      name: row.name,
      displayName: row.display_name,
      description: row.description,
      category: row.category,
      subcategory: row.subcategory,
      icon: row.icon,
      sourceCode: row.source_code,
      credentialCode: row.credential_code,
      documentationMarkdown: row.documentation_markdown,
      documentationUrl: row.documentation_url,
      documentationTitle: row.documentation_title,
      operations: row.operations ? JSON.parse(row.operations) : null,
      apiMethods: row.api_methods ? JSON.parse(row.api_methods) : null,
      documentationExamples: row.documentation_examples ? JSON.parse(row.documentation_examples) : null,
      templates: row.templates ? JSON.parse(row.templates) : null,
      relatedResources: row.related_resources ? JSON.parse(row.related_resources) : null,
      requiredScopes: row.required_scopes ? JSON.parse(row.required_scopes) : null,
      exampleWorkflow: row.example_workflow ? JSON.parse(row.example_workflow) : null,
      exampleParameters: row.example_parameters ? JSON.parse(row.example_parameters) : null,
      propertiesSchema: row.properties_schema ? JSON.parse(row.properties_schema) : null,
      packageName: row.package_name,
      version: row.version,
      codexData: row.codex_data ? JSON.parse(row.codex_data) : null,
      aliases: row.aliases ? JSON.parse(row.aliases) : null,
      hasCredentials: row.has_credentials === 1,
      isTrigger: row.is_trigger === 1,
      isWebhook: row.is_webhook === 1
    };
  }

  /**
   * Generate hash for content
   */
  private generateHash(content: string): string {
    return createHash('sha256').update(content).digest('hex');
  }

  /**
   * Store extraction statistics
   */
  private storeStatistics(stats: any): void {
    if (!this.db) throw new Error('Database not initialized');
    const stmt = this.db.prepare(`
      INSERT INTO extraction_stats (
        total_nodes, nodes_with_docs, nodes_with_examples,
        total_code_size, total_docs_size
      ) VALUES (?, ?, ?, ?, ?)
    `);
    
    // Calculate sizes
    const sizeStats = this.db!.prepare(`
      SELECT 
        COUNT(*) as total,
        SUM(CASE WHEN documentation_markdown IS NOT NULL THEN 1 ELSE 0 END) as with_docs,
        SUM(CASE WHEN example_workflow IS NOT NULL THEN 1 ELSE 0 END) as with_examples,
        SUM(code_length) as code_size,
        SUM(LENGTH(documentation_markdown)) as docs_size
      FROM nodes
    `).get() as any;
    
    stmt.run(
      stats.successful,
      sizeStats?.with_docs || 0,
      sizeStats?.with_examples || 0,
      sizeStats?.code_size || 0,
      sizeStats?.docs_size || 0
    );
  }

  /**
   * Get database statistics
   */
  async getStatistics(): Promise<any> {
    await this.ensureInitialized();
    const stats = this.db!.prepare(`
      SELECT 
        COUNT(*) as totalNodes,
        COUNT(DISTINCT package_name) as totalPackages,
        SUM(code_length) as totalCodeSize,
        SUM(CASE WHEN documentation_markdown IS NOT NULL THEN 1 ELSE 0 END) as nodesWithDocs,
        SUM(CASE WHEN example_workflow IS NOT NULL THEN 1 ELSE 0 END) as nodesWithExamples,
        SUM(has_credentials) as nodesWithCredentials,
        SUM(is_trigger) as triggerNodes,
        SUM(is_webhook) as webhookNodes
      FROM nodes
    `).get() as any;
    
    const packages = this.db!.prepare(`
      SELECT package_name as package, COUNT(*) as count
      FROM nodes
      GROUP BY package_name
      ORDER BY count DESC
    `).all();
    
    return {
      totalNodes: stats?.totalNodes || 0,
      totalPackages: stats?.totalPackages || 0,
      totalCodeSize: stats?.totalCodeSize || 0,
      nodesWithDocs: stats?.nodesWithDocs || 0,
      nodesWithExamples: stats?.nodesWithExamples || 0,
      nodesWithCredentials: stats?.nodesWithCredentials || 0,
      triggerNodes: stats?.triggerNodes || 0,
      webhookNodes: stats?.webhookNodes || 0,
      packageDistribution: packages
    };
  }

  /**
   * Close database connection
   */
  async close(): Promise<void> {
    await this.ensureInitialized();
    this.db!.close();
  }
}