const fs = require('fs-extra');
const path = require('node:path');
const glob = require('glob');
const chalk = require('chalk');
const yaml = require('js-yaml');

/**
 * Dependency Resolver for BMAD modules
 * Handles cross-module dependencies and ensures all required files are included
 */
class DependencyResolver {
  constructor() {
    this.dependencies = new Map();
    this.resolvedFiles = new Set();
    this.missingDependencies = new Set();
  }

  /**
   * Resolve all dependencies for selected modules
   * @param {string} bmadDir - BMAD installation directory
   * @param {Array} selectedModules - Modules explicitly selected by user
   * @param {Object} options - Resolution options
   * @returns {Object} Resolution results with all required files
   */
  async resolve(bmadDir, selectedModules = [], options = {}) {
    if (options.verbose) {
      console.log(chalk.cyan('Resolving module dependencies...'));
    }

    // Always include core as base
    const modulesToProcess = new Set(['core', ...selectedModules]);

    // First pass: collect all explicitly selected files
    const primaryFiles = await this.collectPrimaryFiles(bmadDir, modulesToProcess);

    // Second pass: parse and resolve dependencies
    const allDependencies = await this.parseDependencies(primaryFiles);

    // Third pass: resolve dependency paths and collect files
    const resolvedDeps = await this.resolveDependencyPaths(bmadDir, allDependencies);

    // Fourth pass: check for transitive dependencies
    const transitiveDeps = await this.resolveTransitiveDependencies(bmadDir, resolvedDeps);

    // Combine all files
    const allFiles = new Set([...primaryFiles.map((f) => f.path), ...resolvedDeps, ...transitiveDeps]);

    // Organize by module
    const organizedFiles = this.organizeByModule(bmadDir, allFiles);

    // Report results (only in verbose mode)
    if (options.verbose) {
      this.reportResults(organizedFiles, selectedModules);
    }

    return {
      primaryFiles,
      dependencies: resolvedDeps,
      transitiveDependencies: transitiveDeps,
      allFiles: [...allFiles],
      byModule: organizedFiles,
      missing: [...this.missingDependencies],
    };
  }

  /**
   * Collect primary files from selected modules
   */
  async collectPrimaryFiles(bmadDir, modules) {
    const files = [];

    for (const module of modules) {
      // Handle both source (src/) and installed (bmad/) directory structures
      let moduleDir;

      // Check if this is a source directory (has 'src' subdirectory)
      const srcDir = path.join(bmadDir, 'src');
      if (await fs.pathExists(srcDir)) {
        // Source directory structure: src/core or src/modules/xxx
        moduleDir = module === 'core' ? path.join(srcDir, 'core') : path.join(srcDir, 'modules', module);
      } else {
        // Installed directory structure: bmad/core or bmad/modules/xxx
        moduleDir = module === 'core' ? path.join(bmadDir, 'core') : path.join(bmadDir, 'modules', module);
      }

      if (!(await fs.pathExists(moduleDir))) {
        console.warn(chalk.yellow(`Module directory not found: ${moduleDir}`));
        continue;
      }

      // Collect agents
      const agentsDir = path.join(moduleDir, 'agents');
      if (await fs.pathExists(agentsDir)) {
        const agentFiles = await glob.glob('*.md', { cwd: agentsDir });
        for (const file of agentFiles) {
          const agentPath = path.join(agentsDir, file);

          // Check for localskip attribute
          const content = await fs.readFile(agentPath, 'utf8');
          const hasLocalSkip = content.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
          if (hasLocalSkip) {
            continue; // Skip agents marked for web-only
          }

          files.push({
            path: agentPath,
            type: 'agent',
            module,
            name: path.basename(file, '.md'),
          });
        }
      }

      // Collect tasks
      const tasksDir = path.join(moduleDir, 'tasks');
      if (await fs.pathExists(tasksDir)) {
        const taskFiles = await glob.glob('*.md', { cwd: tasksDir });
        for (const file of taskFiles) {
          files.push({
            path: path.join(tasksDir, file),
            type: 'task',
            module,
            name: path.basename(file, '.md'),
          });
        }
      }
    }

    return files;
  }

  /**
   * Parse dependencies from file content
   */
  async parseDependencies(files) {
    const allDeps = new Set();

    for (const file of files) {
      const content = await fs.readFile(file.path, 'utf8');

      // Parse YAML frontmatter for explicit dependencies
      const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
      if (frontmatterMatch) {
        try {
          // Pre-process to handle backticks in YAML values
          let yamlContent = frontmatterMatch[1];
          // Quote values with backticks to make them valid YAML
          yamlContent = yamlContent.replaceAll(/: `([^`]+)`/g, ': "$1"');

          const frontmatter = yaml.load(yamlContent);
          if (frontmatter.dependencies) {
            const deps = Array.isArray(frontmatter.dependencies) ? frontmatter.dependencies : [frontmatter.dependencies];

            for (const dep of deps) {
              allDeps.add({
                from: file.path,
                dependency: dep,
                type: 'explicit',
              });
            }
          }

          // Check for template dependencies
          if (frontmatter.template) {
            const templates = Array.isArray(frontmatter.template) ? frontmatter.template : [frontmatter.template];
            for (const template of templates) {
              allDeps.add({
                from: file.path,
                dependency: template,
                type: 'template',
              });
            }
          }
        } catch (error) {
          console.warn(chalk.yellow(`Failed to parse frontmatter in ${file.name}: ${error.message}`));
        }
      }

      // Parse content for command references (cross-module dependencies)
      const commandRefs = this.parseCommandReferences(content);
      for (const ref of commandRefs) {
        allDeps.add({
          from: file.path,
          dependency: ref,
          type: 'command',
        });
      }

      // Parse for file path references
      const fileRefs = this.parseFileReferences(content);
      for (const ref of fileRefs) {
        // Determine type based on path format
        // Paths starting with bmad/ are absolute references to the bmad installation
        const depType = ref.startsWith('bmad/') ? 'bmad-path' : 'file';
        allDeps.add({
          from: file.path,
          dependency: ref,
          type: depType,
        });
      }
    }

    return allDeps;
  }

  /**
   * Parse command references from content
   */
  parseCommandReferences(content) {
    const refs = new Set();

    // Match @task-{name} or @agent-{name} or @{module}-{type}-{name}
    const commandPattern = /@(task-|agent-|bmad-)([a-z0-9-]+)/g;
    let match;

    while ((match = commandPattern.exec(content)) !== null) {
      refs.add(match[0]);
    }

    // Match file paths like bmad/core/agents/analyst
    const pathPattern = /bmad\/(core|bmm|cis)\/(agents|tasks)\/([a-z0-9-]+)/g;

    while ((match = pathPattern.exec(content)) !== null) {
      refs.add(match[0]);
    }

    return [...refs];
  }

  /**
   * Parse file path references from content
   */
  parseFileReferences(content) {
    const refs = new Set();

    // Match relative paths like ../templates/file.yaml or ./data/file.md
    const relativePattern = /['"](\.\.?\/[^'"]+\.(md|yaml|yml|xml|json|txt|csv))['"]/g;
    let match;

    while ((match = relativePattern.exec(content)) !== null) {
      refs.add(match[1]);
    }

    // Parse exec attributes in command tags
    const execPattern = /exec="([^"]+)"/g;
    while ((match = execPattern.exec(content)) !== null) {
      let execPath = match[1];
      if (execPath && execPath !== '*') {
        // Remove {project-root} prefix to get the actual path
        // Usage is like {project-root}/bmad/core/tasks/foo.md
        if (execPath.includes('{project-root}')) {
          execPath = execPath.replace('{project-root}', '');
        }
        refs.add(execPath);
      }
    }

    // Parse tmpl attributes in command tags
    const tmplPattern = /tmpl="([^"]+)"/g;
    while ((match = tmplPattern.exec(content)) !== null) {
      let tmplPath = match[1];
      if (tmplPath && tmplPath !== '*') {
        // Remove {project-root} prefix to get the actual path
        // Usage is like {project-root}/bmad/core/tasks/foo.md
        if (tmplPath.includes('{project-root}')) {
          tmplPath = tmplPath.replace('{project-root}', '');
        }
        refs.add(tmplPath);
      }
    }

    return [...refs];
  }

  /**
   * Resolve dependency paths to actual files
   */
  async resolveDependencyPaths(bmadDir, dependencies) {
    const resolved = new Set();

    for (const dep of dependencies) {
      const resolvedPaths = await this.resolveSingleDependency(bmadDir, dep);
      for (const path of resolvedPaths) {
        resolved.add(path);
      }
    }

    return resolved;
  }

  /**
   * Resolve a single dependency to file paths
   */
  async resolveSingleDependency(bmadDir, dep) {
    const paths = [];

    switch (dep.type) {
      case 'explicit':
      case 'file': {
        let depPath = dep.dependency;

        // Handle {project-root} prefix if present
        if (depPath.includes('{project-root}')) {
          // Remove {project-root} and resolve as bmad path
          depPath = depPath.replace('{project-root}', '');

          if (depPath.startsWith('bmad/')) {
            const bmadPath = depPath.replace(/^bmad\//, '');

            // Handle glob patterns
            if (depPath.includes('*')) {
              // Extract the base path and pattern
              const pathParts = bmadPath.split('/');
              const module = pathParts[0];
              const filePattern = pathParts.at(-1);
              const middlePath = pathParts.slice(1, -1).join('/');

              let basePath;
              if (module === 'core') {
                basePath = path.join(bmadDir, 'core', middlePath);
              } else {
                basePath = path.join(bmadDir, 'modules', module, middlePath);
              }

              if (await fs.pathExists(basePath)) {
                const files = await glob.glob(filePattern, { cwd: basePath });
                for (const file of files) {
                  paths.push(path.join(basePath, file));
                }
              }
            } else {
              // Direct path
              if (bmadPath.startsWith('core/')) {
                const corePath = path.join(bmadDir, bmadPath);
                if (await fs.pathExists(corePath)) {
                  paths.push(corePath);
                }
              } else {
                const parts = bmadPath.split('/');
                const module = parts[0];
                const rest = parts.slice(1).join('/');
                const modulePath = path.join(bmadDir, 'modules', module, rest);

                if (await fs.pathExists(modulePath)) {
                  paths.push(modulePath);
                }
              }
            }
          }
        } else {
          // Regular relative path handling
          const sourceDir = path.dirname(dep.from);

          // Handle glob patterns
          if (depPath.includes('*')) {
            const basePath = path.resolve(sourceDir, path.dirname(depPath));
            const pattern = path.basename(depPath);

            if (await fs.pathExists(basePath)) {
              const files = await glob.glob(pattern, { cwd: basePath });
              for (const file of files) {
                paths.push(path.join(basePath, file));
              }
            }
          } else {
            // Direct file reference
            const fullPath = path.resolve(sourceDir, depPath);
            if (await fs.pathExists(fullPath)) {
              paths.push(fullPath);
            } else {
              this.missingDependencies.add(`${depPath} (referenced by ${path.basename(dep.from)})`);
            }
          }
        }

        break;
      }
      case 'command': {
        // Resolve command references to actual files
        const commandPath = await this.resolveCommandToPath(bmadDir, dep.dependency);
        if (commandPath) {
          paths.push(commandPath);
        }

        break;
      }
      case 'bmad-path': {
        // Resolve bmad/ paths (from {project-root}/bmad/... references)
        // These are paths relative to the src directory structure
        const bmadPath = dep.dependency.replace(/^bmad\//, '');

        // Try to resolve as if it's in src structure
        // bmad/core/tasks/foo.md -> src/core/tasks/foo.md
        // bmad/bmm/tasks/bar.md -> src/modules/bmm/tasks/bar.md

        if (bmadPath.startsWith('core/')) {
          const corePath = path.join(bmadDir, bmadPath);
          if (await fs.pathExists(corePath)) {
            paths.push(corePath);
          } else {
            // Not found, but don't report as missing since it might be installed later
          }
        } else {
          // It's a module path like bmm/tasks/foo.md or cis/agents/bar.md
          const parts = bmadPath.split('/');
          const module = parts[0];
          const rest = parts.slice(1).join('/');
          const modulePath = path.join(bmadDir, 'modules', module, rest);

          if (await fs.pathExists(modulePath)) {
            paths.push(modulePath);
          } else {
            // Not found, but don't report as missing since it might be installed later
          }
        }

        break;
      }
      case 'template': {
        // Resolve template references
        let templateDep = dep.dependency;

        // Handle {project-root} prefix if present
        if (templateDep.includes('{project-root}')) {
          // Remove {project-root} and treat as bmad-path
          templateDep = templateDep.replace('{project-root}', '');

          // Now resolve as a bmad path
          if (templateDep.startsWith('bmad/')) {
            const bmadPath = templateDep.replace(/^bmad\//, '');

            if (bmadPath.startsWith('core/')) {
              const corePath = path.join(bmadDir, bmadPath);
              if (await fs.pathExists(corePath)) {
                paths.push(corePath);
              }
            } else {
              // Module path like cis/templates/brainstorm.md
              const parts = bmadPath.split('/');
              const module = parts[0];
              const rest = parts.slice(1).join('/');
              const modulePath = path.join(bmadDir, 'modules', module, rest);

              if (await fs.pathExists(modulePath)) {
                paths.push(modulePath);
              }
            }
          }
        } else {
          // Regular relative template path
          const sourceDir = path.dirname(dep.from);
          const templatePath = path.resolve(sourceDir, templateDep);

          if (await fs.pathExists(templatePath)) {
            paths.push(templatePath);
          } else {
            this.missingDependencies.add(`Template: ${dep.dependency}`);
          }
        }

        break;
      }
      // No default
    }

    return paths;
  }

  /**
   * Resolve command reference to file path
   */
  async resolveCommandToPath(bmadDir, command) {
    // Parse command format: @task-name or @agent-name or bmad/module/type/name

    if (command.startsWith('@task-')) {
      const taskName = command.slice(6);
      // Search all modules for this task
      for (const module of ['core', 'bmm', 'cis']) {
        const taskPath =
          module === 'core'
            ? path.join(bmadDir, 'core', 'tasks', `${taskName}.md`)
            : path.join(bmadDir, 'modules', module, 'tasks', `${taskName}.md`);
        if (await fs.pathExists(taskPath)) {
          return taskPath;
        }
      }
    } else if (command.startsWith('@agent-')) {
      const agentName = command.slice(7);
      // Search all modules for this agent
      for (const module of ['core', 'bmm', 'cis']) {
        const agentPath =
          module === 'core'
            ? path.join(bmadDir, 'core', 'agents', `${agentName}.md`)
            : path.join(bmadDir, 'modules', module, 'agents', `${agentName}.md`);
        if (await fs.pathExists(agentPath)) {
          return agentPath;
        }
      }
    } else if (command.startsWith('bmad/')) {
      // Direct path reference
      const parts = command.split('/');
      if (parts.length >= 4) {
        const [, module, type, ...nameParts] = parts;
        const name = nameParts.join('/'); // Handle nested paths

        // Check if name already has extension
        const fileName = name.endsWith('.md') ? name : `${name}.md`;

        const filePath =
          module === 'core' ? path.join(bmadDir, 'core', type, fileName) : path.join(bmadDir, 'modules', module, type, fileName);
        if (await fs.pathExists(filePath)) {
          return filePath;
        }
      }
    }

    // Don't report as missing if it's a self-reference within the module being installed
    if (!command.includes('cis') || command.includes('brain')) {
      // Only report missing if it's a true external dependency
      // this.missingDependencies.add(`Command: ${command}`);
    }
    return null;
  }

  /**
   * Resolve transitive dependencies (dependencies of dependencies)
   */
  async resolveTransitiveDependencies(bmadDir, directDeps) {
    const transitive = new Set();
    const processed = new Set();

    // Process each direct dependency
    for (const depPath of directDeps) {
      if (processed.has(depPath)) continue;
      processed.add(depPath);

      // Only process markdown and YAML files for transitive deps
      if ((depPath.endsWith('.md') || depPath.endsWith('.yaml') || depPath.endsWith('.yml')) && (await fs.pathExists(depPath))) {
        const content = await fs.readFile(depPath, 'utf8');
        const subDeps = await this.parseDependencies([
          {
            path: depPath,
            type: 'dependency',
            module: this.getModuleFromPath(bmadDir, depPath),
            name: path.basename(depPath),
          },
        ]);

        const resolvedSubDeps = await this.resolveDependencyPaths(bmadDir, subDeps);
        for (const subDep of resolvedSubDeps) {
          if (!directDeps.has(subDep)) {
            transitive.add(subDep);
          }
        }
      }
    }

    return transitive;
  }

  /**
   * Get module name from file path
   */
  getModuleFromPath(bmadDir, filePath) {
    const relative = path.relative(bmadDir, filePath);
    const parts = relative.split(path.sep);

    // Handle source directory structure (src/core or src/modules/xxx)
    if (parts[0] === 'src') {
      if (parts[1] === 'core') {
        return 'core';
      } else if (parts[1] === 'modules' && parts.length > 2) {
        return parts[2];
      }
    }

    // Check if it's in modules directory (installed structure)
    if (parts[0] === 'modules' && parts.length > 1) {
      return parts[1];
    }

    // Otherwise return the first part (core, etc.)
    // But don't return 'src' as a module name
    if (parts[0] === 'src') {
      return 'unknown';
    }
    return parts[0] || 'unknown';
  }

  /**
   * Organize files by module
   */
  organizeByModule(bmadDir, files) {
    const organized = {};

    for (const file of files) {
      const module = this.getModuleFromPath(bmadDir, file);
      if (!organized[module]) {
        organized[module] = {
          agents: [],
          tasks: [],
          tools: [],
          templates: [],
          data: [],
          other: [],
        };
      }

      // Get relative path correctly based on module structure
      let moduleBase;

      // Check if file is in source directory structure
      if (file.includes('/src/core/') || file.includes('/src/modules/')) {
        moduleBase = module === 'core' ? path.join(bmadDir, 'src', 'core') : path.join(bmadDir, 'src', 'modules', module);
      } else {
        // Installed structure
        moduleBase = module === 'core' ? path.join(bmadDir, 'core') : path.join(bmadDir, 'modules', module);
      }

      const relative = path.relative(moduleBase, file);

      // Check file path for categorization
      // Brain-tech files are data, not tasks (even though they're in tasks/brain-tech/)
      if (file.includes('/brain-tech/')) {
        organized[module].data.push(file);
      } else if (relative.startsWith('agents/') || file.includes('/agents/')) {
        organized[module].agents.push(file);
      } else if (relative.startsWith('tasks/') || file.includes('/tasks/')) {
        organized[module].tasks.push(file);
      } else if (relative.startsWith('tools/') || file.includes('/tools/')) {
        organized[module].tools.push(file);
      } else if (relative.includes('template') || file.includes('/templates/')) {
        organized[module].templates.push(file);
      } else if (relative.includes('data/')) {
        organized[module].data.push(file);
      } else {
        organized[module].other.push(file);
      }
    }

    return organized;
  }

  /**
   * Report resolution results
   */
  reportResults(organized, selectedModules) {
    console.log(chalk.green('\n✓ Dependency resolution complete'));

    for (const [module, files] of Object.entries(organized)) {
      const isSelected = selectedModules.includes(module) || module === 'core';
      const totalFiles =
        files.agents.length + files.tasks.length + files.tools.length + files.templates.length + files.data.length + files.other.length;

      if (totalFiles > 0) {
        console.log(chalk.cyan(`\n  ${module.toUpperCase()} module:`));
        console.log(chalk.dim(`    Status: ${isSelected ? 'Selected' : 'Dependencies only'}`));

        if (files.agents.length > 0) {
          console.log(chalk.dim(`    Agents: ${files.agents.length}`));
        }
        if (files.tasks.length > 0) {
          console.log(chalk.dim(`    Tasks: ${files.tasks.length}`));
        }
        if (files.templates.length > 0) {
          console.log(chalk.dim(`    Templates: ${files.templates.length}`));
        }
        if (files.data.length > 0) {
          console.log(chalk.dim(`    Data files: ${files.data.length}`));
        }
        if (files.other.length > 0) {
          console.log(chalk.dim(`    Other files: ${files.other.length}`));
        }
      }
    }

    if (this.missingDependencies.size > 0) {
      console.log(chalk.yellow('\n  ⚠ Missing dependencies:'));
      for (const missing of this.missingDependencies) {
        console.log(chalk.yellow(`    - ${missing}`));
      }
    }
  }

  /**
   * Create a bundle for web deployment
   * @param {Object} resolution - Resolution results from resolve()
   * @returns {Object} Bundle data ready for web
   */
  async createWebBundle(resolution) {
    const bundle = {
      metadata: {
        created: new Date().toISOString(),
        modules: Object.keys(resolution.byModule),
        totalFiles: resolution.allFiles.length,
      },
      agents: {},
      tasks: {},
      templates: {},
      data: {},
    };

    // Bundle all files by type
    for (const filePath of resolution.allFiles) {
      if (!(await fs.pathExists(filePath))) continue;

      const content = await fs.readFile(filePath, 'utf8');
      const relative = path.relative(path.dirname(resolution.primaryFiles[0]?.path || '.'), filePath);

      if (filePath.includes('/agents/')) {
        bundle.agents[relative] = content;
      } else if (filePath.includes('/tasks/')) {
        bundle.tasks[relative] = content;
      } else if (filePath.includes('template')) {
        bundle.templates[relative] = content;
      } else {
        bundle.data[relative] = content;
      }
    }

    return bundle;
  }
}

module.exports = { DependencyResolver };
