/**
 * Generate Code Node - Generate code implementation with optional commit/push
 * Used by: analysisGraph (preview mode), implementationGraph (commit mode)
 * Also generates PR title and description (Why/How format) for use when creating PRs.
 */

import { spawn } from 'child_process';
import { join, resolve } from 'path';
import { existsSync, readFileSync } from 'fs';
import Handlebars from 'handlebars';
import { invokeAgent } from '@zibby/core';
import { generatePRMeta } from './services/prMetaService.js';
import { adfToText } from '../../../dist/utils/adf-converter.js';
import { z } from 'zod';

const CodeImplementationOutputSchema = z.object({
  success: z.boolean(),
  codeImplementation: z.object({
    branchName: z.string().optional(),
    committedRepos: z.array(z.string()).optional(),
    diff: z.string(),
    diffStat: z.string(),
    changedFiles: z.array(z.string()),
    fileContents: z.array(z.object({
      path: z.string(),
      content: z.string()
    })),
    prTitle: z.string().optional(),
    prDescription: z.string().optional(),
    metrics: z.object({
      filesChanged: z.number(),
      additions: z.number(),
      deletions: z.number(),
      netChange: z.number()
    }),
    commitMessage: z.string().optional(),
    agentOutput: z.string(),
    committed: z.boolean(),
    timestamp: z.string()
  })
});

/**
 * Factory function to create code generation node
 * @param {Object} options - Configuration options
 * @param {boolean} options.commitAndPush - If true, creates branch, commits, and pushes
 * @param {string} options.nodeName - Name for the node (default: 'generate_code')
 */
export function createCodeGenerationNode(options = {}) {
  const { 
    commitAndPush = false,
    nodeName = 'generate_code'
  } = options;

  return {
    name: nodeName,
    outputSchema: CodeImplementationOutputSchema,
    execute: async (state) => {
      const mode = commitAndPush ? 'implementing' : 'generating preview of';
      console.log(`\n💻 ${commitAndPush ? 'Implementing' : 'Generating'} code implementation...`);
      
      const { workspace, ticketContext, repos, promptsDir, model, nodeConfigs = {} } = state;
      const aiModel = model || ticketContext.model || 'auto';
      const _nodeConfig = nodeConfigs[nodeName] || {};
      const analysis = state.analyze_ticket?.analysis;
      
      // Build implementation prompt from template
      const implementPrompt = buildImplementationPrompt(
        promptsDir,
        ticketContext,
        analysis,
        commitAndPush,
        repos,
        workspace
      );
      
      console.log(`🚀 Running AI Agent to ${mode} changes with model: ${aiModel}...`);
      
      const implementOutput = await invokeAgent(implementPrompt, { state, model: aiModel });
      
      let branchName = null;
      let commitMessage = null;
      const committedRepos = [];
      
      // If commit mode, create branch and commit in every repo that has changes
      if (commitAndPush) {
        const reposToCommit = repos && repos.length > 0 ? repos : [];
        if (reposToCommit.length === 0) {
          throw new Error('No repositories configured for commit/push');
        }
        
        branchName = `feature/${ticketContext.ticketKey.toLowerCase()}-${generateShortId()}`;
        const commitDesc = typeof ticketContext.description === 'object'
          ? adfToText(ticketContext.description)
          : (ticketContext.description || 'No description');
        commitMessage = `${ticketContext.ticketKey}: ${ticketContext.summary}\n\n${commitDesc}\n\nImplemented by Zibby Agent`;

        for (const repo of reposToCommit) {
          const repoPath = join(workspace, repo.name);
          
          // Check if this repo has any changes
          const status = await execCommand('git', ['status', '--porcelain'], repoPath);
          if (!status.trim()) {
            console.log(`⏭️ No changes in ${repo.name}, skipping`);
            continue;
          }
          
          console.log(`📊 Committing changes in ${repo.name}...`);
          
          await execCommand('git', ['checkout', '-b', branchName], repoPath);
          await execCommand('git', ['add', '.'], repoPath);
          await execCommand('git', ['commit', '-m', commitMessage], repoPath);
          
          // Set remote URL with token for push
          const githubToken = state.githubToken;
          const gitlabToken = process.env.GITLAB_TOKEN || '';
          const gitlabUrl = process.env.GITLAB_URL || '';
          
          if (repo.provider === 'gitlab' && gitlabToken && gitlabUrl) {
            try {
              const gitlabHost = new URL(gitlabUrl).host;
              const remoteUrl = repo.url.replace(`https://${gitlabHost}`, `https://oauth2:${gitlabToken}@${gitlabHost}`);
              await execCommand('git', ['remote', 'set-url', 'origin', remoteUrl], repoPath);
            } catch (e) {
              console.warn(`⚠️ Failed to set GitLab remote for ${repo.name}: ${e.message}`);
            }
          } else if (githubToken && repo.url?.includes('github.com')) {
            const remoteUrl = repo.url.replace('https://github.com', `https://x-access-token:${githubToken}@github.com`);
            await execCommand('git', ['remote', 'set-url', 'origin', remoteUrl], repoPath);
          }
          
          console.log(`📤 Pushing ${repo.name} to remote...`);
          await execCommand('git', ['push', '-u', 'origin', branchName], repoPath);
          committedRepos.push(repo.name);
        }
        
        if (committedRepos.length === 0) {
          console.warn('⚠️ No repos had changes to commit');
        }
      }
      
      console.log('📊 Capturing code changes...');
      
      // Capture git diff from ALL repos (multi-repo support)
      // - Commit mode: compare last commit to previous (HEAD~1)
      // - Preview mode: show all uncommitted changes (staged + unstaged)
      const allDiffs = [];
      const allDiffStats = [];
      const allChangedFiles = [];
      
      const reposToCheck = repos && repos.length > 0 
        ? repos 
        : [{ name: '.' }];
      
      for (const repo of reposToCheck) {
        const currentRepoPath = repo.name === '.' 
          ? workspace 
          : join(workspace, repo.name);
        
        try {
          let repoDiff, repoDiffStat, repoChangedFiles;
          
          if (commitAndPush) {
            repoDiff = await execCommand('git', ['diff', '-U10', 'HEAD~1'], currentRepoPath);
            repoDiffStat = await execCommand('git', ['diff', '--stat', 'HEAD~1'], currentRepoPath);
            const diffNameOutput = await execCommand('git', ['diff', '--name-only', 'HEAD~1'], currentRepoPath);
            repoChangedFiles = diffNameOutput.split('\n').filter(f => f.trim());
          } else {
            repoDiff = await execCommand('git', ['diff', '-U10', 'HEAD'], currentRepoPath);
            repoDiffStat = await execCommand('git', ['diff', '--stat', 'HEAD'], currentRepoPath);
            
            const modifiedOutput = await execCommand('git', ['diff', '--name-only', 'HEAD'], currentRepoPath);
            const modifiedFiles = modifiedOutput.split('\n').filter(f => f.trim());
            const untrackedOutput = await execCommand('git', ['ls-files', '--others', '--exclude-standard'], currentRepoPath);
            const untrackedFiles = untrackedOutput.split('\n').filter(f => f.trim());
            repoChangedFiles = [...new Set([...modifiedFiles, ...untrackedFiles])];
          }
          
          const prefix = repos && repos.length > 1 ? `${repo.name}/` : '';
          
          if (repoDiff.trim()) {
            allDiffs.push(`# ${repo.name}\n${repoDiff}`);
          }
          if (repoDiffStat.trim()) {
            allDiffStats.push(`# ${repo.name}\n${repoDiffStat}`);
          }
          if (repoChangedFiles.length > 0) {
            allChangedFiles.push(...repoChangedFiles.map(f => `${prefix}${f}`));
          }
        } catch (err) {
          console.warn(`⚠️ Could not capture diff for repo ${repo.name}:`, err.message);
        }
      }
      
      const diff = allDiffs.join('\n\n');
      const diffStat = allDiffStats.join('\n');
      const changedFiles = allChangedFiles;
      
      // Store file contents for expandable diff view
      // Only store for reasonable number of files to avoid huge payloads
      const fileContents = [];
      if (changedFiles.length <= 20) {
        for (const filePath of changedFiles) {
          try {
            let fullPath;
            if (repos && repos.length > 1) {
              fullPath = resolve(workspace, filePath);
            } else {
              const repoDir = repos?.[0] ? resolve(workspace, repos[0].name) : workspace;
              fullPath = resolve(repoDir, filePath);
            }
            
            if (!fullPath.startsWith(resolve(workspace))) continue;
            if (existsSync(fullPath)) {
              const content = readFileSync(fullPath, 'utf-8');
              if (content.length < 100000) {
                fileContents.push({ path: filePath, content });
              }
            }
          } catch (_err) {
            // Skip files we can't read
          }
        }
      }
      
      // Parse diff for metrics
      const additions = (diff.match(/^\+[^+]/gm) || []).length;
      const deletions = (diff.match(/^-[^-]/gm) || []).length;
      const filesChanged = changedFiles.length;
      
      // Generate PR title and description (Why/How format) when we have changes
      let prTitle = null;
      let prDescription = null;
      if (diff.trim() && ticketContext?.ticketKey) {
        console.log('📝 Generating PR title and description...');
        try {
          const prMeta = await generatePRMeta({
            ticketContext,
            changedFiles,
            diffStat,
            state,
            model: aiModel
          }).catch(err => {
            console.error('   ⚠️  Promise rejection in generatePRMeta:', err.message);
            throw err;
          });
          
          console.log('   🔍 generatePRMeta returned:', typeof prMeta, prMeta ? Object.keys(prMeta) : 'null/undefined');
          
          if (prMeta && typeof prMeta === 'object') {
            prTitle = prMeta.prTitle;
            prDescription = prMeta.prDescription;
            console.log(`   ✅ PR title: ${prTitle}`);
            console.log(`   ✅ PR description preview: ${prDescription?.substring(0, 100)}...`);
          } else {
            console.error('   ⚠️  generatePRMeta returned invalid result:', prMeta);
          }
        } catch (err) {
          console.error('   ❌ Failed to generate PR meta:', err.message);
          console.error('   Error details:', err.stack);
        } finally {
          console.log('   ✅ PR meta generation attempt complete');
        }
      } else {
        console.log('📝 Skipping PR meta generation (no diff or no ticketKey)');
      }

      console.log(`✅ Code ${commitAndPush ? 'implementation' : 'generation'} complete`);
      if (branchName) {
        console.log(`   📁 Branch: ${branchName}`);
        if (committedRepos.length > 0) {
          console.log(`   📦 Committed repos: ${committedRepos.join(', ')}`);
        }
      }
      console.log(`   📁 Files changed: ${filesChanged}`);
      console.log(`   ➕ Additions: ${additions} lines`);
      console.log(`   ➖ Deletions: ${deletions} lines`);

      return {
        success: true,
        codeImplementation: {
          ...(branchName && { branchName }),
          ...(committedRepos.length > 0 && { committedRepos }),
          diff,
          diffStat,
          changedFiles,
          fileContents,
          ...(prTitle && { prTitle }),
          ...(prDescription && { prDescription }),
          metrics: {
            filesChanged,
            additions,
            deletions,
            netChange: additions - deletions
          },
          ...(commitMessage && { commitMessage }),
          agentOutput: implementOutput,
          committed: commitAndPush,
          timestamp: new Date().toISOString()
        }
      };
    }
  };
}

// Convenience exports for common use cases
export const generateCodeNode = createCodeGenerationNode({ 
  commitAndPush: false,
  nodeName: 'generate_code'
});

export const implementCodeNode = createCodeGenerationNode({ 
  commitAndPush: true,
  nodeName: 'implement_code'
});

function buildImplementationPrompt(promptsDir, ticketContext, analysis, isCommitting, repos, workspace) {
  const templatePath = join(promptsDir, 'generate-code.md');
  if (!existsSync(templatePath)) {
    throw new Error(`Template not found: ${templatePath}`);
  }
  
  const templateContent = readFileSync(templatePath, 'utf-8');
  const template = Handlebars.compile(templateContent);
  
  let descriptionText = ticketContext.description || 'No description provided';
  console.log(`🔍 [generateCode] description type: ${typeof descriptionText}, value: ${typeof descriptionText === 'object' ? JSON.stringify(descriptionText).slice(0, 200) : String(descriptionText).slice(0, 200)}`);
  if (typeof descriptionText === 'object') {
    descriptionText = adfToText(descriptionText);
    console.log(`🔍 [generateCode] Converted ADF to text (${descriptionText.length} chars): ${descriptionText.slice(0, 200)}...`);
  }

  let planContext = null;
  if (analysis) {
    try {
      const parsed = typeof analysis.raw === 'string' ? JSON.parse(analysis.raw) : analysis.raw;
      const inner = parsed?.analysis?.structured || parsed;
      planContext = inner?.implementationPlan || null;
    } catch (_) {}
    if (!planContext) {
      planContext = typeof analysis.raw === 'string' ? analysis.raw.slice(0, 3000) : null;
    }
  }

  // Build repo metadata for the template
  const repositories = (repos && repos.length > 0) ? repos.map(r => {
    const info = detectRepoLanguage(workspace ? join(workspace, r.name) : null);
    return { name: r.name, ...info };
  }) : null;

  return template({
    ticketKey: ticketContext.ticketKey,
    ticketSummary: ticketContext.summary,
    ticketDescription: descriptionText,
    acceptanceCriteria: ticketContext.acceptanceCriteria || null,
    analysisContext: planContext,
    repositories,
    isCommitting
  });
}

function detectRepoLanguage(repoPath) {
  if (!repoPath) return { language: 'Unknown', framework: null };
  try {
    if (existsSync(join(repoPath, 'package.json'))) {
      const pkg = JSON.parse(readFileSync(join(repoPath, 'package.json'), 'utf-8'));
      const framework = pkg.dependencies?.react ? 'React'
        : pkg.dependencies?.next ? 'Next.js'
        : pkg.dependencies?.vue ? 'Vue'
        : pkg.dependencies?.express ? 'Express'
        : null;
      return { language: 'JavaScript/TypeScript', framework };
    }
    if (existsSync(join(repoPath, 'pom.xml')) || existsSync(join(repoPath, 'build.gradle'))) {
      return { language: 'Java', framework: null };
    }
    if (existsSync(join(repoPath, 'requirements.txt')) || existsSync(join(repoPath, 'setup.py'))) {
      return { language: 'Python', framework: null };
    }
  } catch (_) {}
  return { language: 'Unknown', framework: null };
}

function generateShortId() {
  return Math.random().toString(36).substring(2, 6);
}

/**
 * Safe spawn wrapper — takes an explicit args array, never uses shell: true.
 * @param {string} bin  - executable (e.g. 'git')
 * @param {string[]} args - argument list
 * @param {string} cwd  - working directory
 */
async function execCommand(bin, args, cwd) {
  return new Promise((res, reject) => {
    const proc = spawn(bin, args, { cwd, shell: false, stdio: ['ignore', 'pipe', 'pipe'] });

    let stdout = '';
    let stderr = '';

    proc.stdout.on('data', (data) => {
      const output = data.toString();
      stdout += output;
      console.log(output.trimEnd());
    });

    proc.stderr.on('data', (data) => {
      const output = data.toString();
      stderr += output;
      console.log(output.trimEnd());
    });

    proc.on('close', (code) => {
      if (code !== 0) {
        reject(new Error(`Command failed (exit ${code}): ${bin} ${args.join(' ')}`));
      } else {
        res(stdout || stderr || '');
      }
    });

    proc.on('error', (err) => {
      reject(new Error(`Command error: ${bin} - ${err.message}`));
    });
  });
}
