/**
 * Vigthoria CLI - Preview Command
 *
 * Local preview server + consolidated visual diffs + Template Service proof gate
 *
 * Usage:
 *   vigthoria preview                     - Preview project in browser
 *   vigthoria preview --diff              - Show consolidated diff of recent agent changes
 *   vigthoria preview --proof             - Run Template Service preview gate
 *   vigthoria preview -p /path/to/project - Preview specific project
 */
import chalk from 'chalk';
import * as fs from 'fs';
import * as path from 'path';
import * as http from 'http';
import { createRequire } from 'node:module';
import { structuredPatch } from 'diff';
import { createSpinner, CH } from '../utils/logger.js';
import { APIClient } from '../utils/api.js';
const require = createRequire(import.meta.url);
// Common MIME types for static file serving
const MIME_TYPES = {
    '.html': 'text/html',
    '.htm': 'text/html',
    '.css': 'text/css',
    '.js': 'application/javascript',
    '.mjs': 'application/javascript',
    '.json': 'application/json',
    '.png': 'image/png',
    '.jpg': 'image/jpeg',
    '.jpeg': 'image/jpeg',
    '.gif': 'image/gif',
    '.svg': 'image/svg+xml',
    '.ico': 'image/x-icon',
    '.webp': 'image/webp',
    '.woff': 'font/woff',
    '.woff2': 'font/woff2',
    '.ttf': 'font/ttf',
    '.eot': 'application/vnd.ms-fontobject',
    '.mp4': 'video/mp4',
    '.webm': 'video/webm',
    '.mp3': 'audio/mpeg',
    '.wav': 'audio/wav',
    '.pdf': 'application/pdf',
    '.xml': 'application/xml',
    '.txt': 'text/plain',
    '.map': 'application/json',
};
export class PreviewCommand {
    config;
    logger;
    api;
    server = null;
    constructor(config, logger) {
        this.config = config;
        this.logger = logger;
        this.api = new APIClient(config, logger);
    }
    async run(options) {
        const projectPath = path.resolve(options.project || process.cwd());
        if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) {
            this.logger.error(`Project directory not found: ${projectPath}`);
            this.api.destroy();
            return;
        }
        console.log();
        console.log(chalk.bold.white(`  ${CH.hLine.repeat(3)} Vigthoria Preview ${CH.hLine.repeat(40)}`));
        console.log(chalk.gray(`  Project: ${projectPath}`));
        console.log();
        // Show consolidated diff of recent agent changes
        if (options.diff) {
            await this.showConsolidatedDiff(projectPath);
        }
        // Run Template Service preview proof gate
        if (options.proof) {
            await this.runProofGate(projectPath, options.screenshot);
        }
        // If only --diff or --proof was requested (no explicit entry/port), just exit
        if (options.diff || options.proof) {
            if (!options.entry && !options.port) {
                this.api.destroy();
                return;
            }
        }
        // Detect entry file
        const entryFile = this.detectEntryFile(projectPath, options.entry);
        if (!entryFile) {
            this.logger.warn('No HTML entry file found. Use --entry <file> to specify one.');
            if (!options.diff && !options.proof) {
                this.api.destroy();
                return;
            }
            this.api.destroy();
            return;
        }
        // Start local preview server
        const port = options.port || (await this.findAvailablePort(3500));
        await this.startServer(projectPath, entryFile, port, options.open !== false);
    }
    /**
     * Detect the HTML entry file in the project
     */
    detectEntryFile(projectPath, specified) {
        if (specified) {
            const fullPath = path.join(projectPath, specified);
            if (fs.existsSync(fullPath))
                return specified;
            this.logger.warn(`Specified entry file not found: ${specified}`);
            return null;
        }
        // Common entry point candidates
        const candidates = [
            'index.html',
            'public/index.html',
            'dist/index.html',
            'build/index.html',
            'src/index.html',
            'out/index.html',
        ];
        for (const candidate of candidates) {
            if (fs.existsSync(path.join(projectPath, candidate))) {
                return candidate;
            }
        }
        // Fallback: find any .html file at root
        try {
            const rootFiles = fs.readdirSync(projectPath);
            const htmlFile = rootFiles.find(f => f.endsWith('.html'));
            if (htmlFile)
                return htmlFile;
        }
        catch { /* ignore */ }
        return null;
    }
    /**
     * Find an available port starting from the given number
     */
    findAvailablePort(startPort) {
        return new Promise((resolve) => {
            const testServer = http.createServer();
            testServer.on('error', () => {
                resolve(this.findAvailablePort(startPort + 1));
            });
            testServer.listen(startPort, () => {
                testServer.close(() => resolve(startPort));
            });
        });
    }
    /**
     * Start a local HTTP server for preview
     */
    async startServer(projectPath, entryFile, port, autoOpen) {
        return new Promise((resolve) => {
            this.server = http.createServer((req, res) => {
                // Sanitize URL to prevent path traversal
                const urlPath = decodeURIComponent(new URL(req.url || '/', `http://localhost:${port}`).pathname);
                const safePath = path.normalize(urlPath).replace(/^(\.\.[/\\])+/, '');
                let filePath = path.join(projectPath, safePath);
                // Ensure the resolved path is within the project directory
                const resolvedPath = path.resolve(filePath);
                if (!resolvedPath.startsWith(path.resolve(projectPath))) {
                    res.writeHead(403);
                    res.end('Forbidden');
                    return;
                }
                // Directory → serve index.html
                if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
                    filePath = path.join(filePath, 'index.html');
                }
                // SPA fallback
                if (!fs.existsSync(filePath)) {
                    const spaFallback = path.join(projectPath, entryFile);
                    if (fs.existsSync(spaFallback)) {
                        filePath = spaFallback;
                    }
                    else {
                        res.writeHead(404);
                        res.end('Not found');
                        return;
                    }
                }
                const ext = path.extname(filePath).toLowerCase();
                const contentType = MIME_TYPES[ext] || 'application/octet-stream';
                try {
                    const content = fs.readFileSync(filePath);
                    res.writeHead(200, { 'Content-Type': contentType });
                    res.end(content);
                }
                catch {
                    res.writeHead(500);
                    res.end('Internal server error');
                }
            });
            this.server.listen(port, () => {
                const url = `http://localhost:${port}/${entryFile}`;
                console.log(chalk.green(`  ${CH.success} Preview server running`));
                console.log(chalk.gray(`  URL: `) + chalk.cyan.underline(url));
                console.log(chalk.gray(`  Entry: ${entryFile}`));
                console.log(chalk.gray(`  Press Ctrl+C to stop`));
                console.log();
                if (autoOpen) {
                    this.openBrowser(url);
                }
            });
            // Handle graceful shutdown
            const shutdown = () => {
                console.log(chalk.gray('\n  Stopping preview server...'));
                this.server?.close();
                this.api.destroy();
                resolve();
            };
            process.on('SIGINT', shutdown);
            process.on('SIGTERM', shutdown);
        });
    }
    /**
     * Open URL in default browser
     */
    openBrowser(url) {
        const { execFile } = require('child_process');
        const platform = process.platform;
        // Validate URL scheme before passing to OS — prevents shell injection via crafted URLs
        try {
            const parsed = new URL(url);
            if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
                return;
            const safeUrl = parsed.href;
            if (platform === 'darwin') {
                execFile('open', [safeUrl], (err) => {
                    if (err)
                        this.logger.debug(`Could not auto-open browser: ${err.message}`);
                });
            }
            else if (platform === 'win32') {
                execFile('cmd', ['/c', 'start', '', safeUrl], (err) => {
                    if (err)
                        this.logger.debug(`Could not auto-open browser: ${err.message}`);
                });
            }
            else {
                execFile('xdg-open', [safeUrl], (err) => {
                    if (err)
                        this.logger.debug(`Could not auto-open browser: ${err.message}`);
                });
            }
        }
        catch {
            this.logger.debug(`Skipping browser open — invalid URL: ${url}`);
        }
    }
    /**
     * Show consolidated diff of recent agent changes using git
     */
    async showConsolidatedDiff(projectPath) {
        console.log(chalk.bold.white(`  ${CH.hLine.repeat(3)} Change Summary ${CH.hLine.repeat(43)}`));
        console.log();
        const proofDir = path.join(projectPath, '.vigthoria', 'proof', 'preview');
        const hasProof = fs.existsSync(proofDir);
        // Try git diff first (most reliable)
        try {
            const { execSync } = require('child_process');
            const isGit = fs.existsSync(path.join(projectPath, '.git'));
            if (isGit) {
                // Get list of changed files (unstaged + staged)
                const statusOutput = execSync('git status --porcelain', { cwd: projectPath, encoding: 'utf-8' }).trim();
                if (!statusOutput) {
                    console.log(chalk.gray('  No changes detected (working tree clean).'));
                    console.log();
                    return;
                }
                const changedLines = statusOutput.split('\n').filter(Boolean);
                const created = [];
                const modified = [];
                const deleted = [];
                for (const line of changedLines) {
                    const status = line.substring(0, 2).trim();
                    const filePath = line.substring(3);
                    if (status === '??' || status === 'A')
                        created.push(filePath);
                    else if (status === 'D')
                        deleted.push(filePath);
                    else
                        modified.push(filePath);
                }
                // Summary header
                const total = created.length + modified.length + deleted.length;
                console.log(chalk.white(`  ${total} file${total !== 1 ? 's' : ''} changed:`));
                for (const f of created)
                    console.log(chalk.green(`    + ${f}`));
                for (const f of modified)
                    console.log(chalk.yellow(`    ~ ${f}`));
                for (const f of deleted)
                    console.log(chalk.red(`    - ${f}`));
                console.log();
                // Show unified diffs for modified and created files (limit to keep terminal manageable)
                const diffTargets = [...modified, ...created].slice(0, 20);
                for (const relPath of diffTargets) {
                    const absPath = path.join(projectPath, relPath);
                    if (!fs.existsSync(absPath))
                        continue;
                    const ext = path.extname(relPath).toLowerCase();
                    const textExts = ['.html', '.css', '.js', '.ts', '.jsx', '.tsx', '.json', '.md', '.yml', '.yaml', '.txt', '.xml', '.svg', '.py', '.sh', '.env'];
                    if (!textExts.includes(ext))
                        continue;
                    try {
                        const newContent = fs.readFileSync(absPath, 'utf-8');
                        let oldContent = '';
                        if (modified.includes(relPath)) {
                            try {
                                // Validate relPath is a safe relative path before interpolating — prevents injection
                                if (/^[a-zA-Z0-9_\-./]+$/.test(relPath) && !relPath.includes('..')) {
                                    oldContent = execSync(`git show HEAD:${relPath}`, { cwd: projectPath, encoding: 'utf-8' });
                                }
                            }
                            catch {
                                // File is new or not tracked
                            }
                        }
                        const patch = structuredPatch(relPath, relPath, oldContent, newContent, 'before', 'after', { context: 3 });
                        if (patch.hunks.length === 0)
                            continue;
                        console.log(chalk.bold.white(`  ${CH.hLine.repeat(3)} ${relPath} ${CH.hLine.repeat(Math.max(1, 50 - relPath.length))}`));
                        console.log(chalk.gray(`  --- a/${relPath}`));
                        console.log(chalk.gray(`  +++ b/${relPath}`));
                        for (const hunk of patch.hunks) {
                            console.log(chalk.cyan(`  @@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`));
                            for (const line of hunk.lines) {
                                if (line.startsWith('+')) {
                                    console.log(chalk.green(`  ${line}`));
                                }
                                else if (line.startsWith('-')) {
                                    console.log(chalk.red(`  ${line}`));
                                }
                                else {
                                    console.log(chalk.gray(`  ${line}`));
                                }
                            }
                        }
                        console.log();
                    }
                    catch {
                        // Skip files that can't be diffed
                    }
                }
                if (diffTargets.length < modified.length + created.length) {
                    console.log(chalk.gray(`  ... and ${modified.length + created.length - diffTargets.length} more files`));
                    console.log();
                }
                return;
            }
        }
        catch {
            // git not available, fall through
        }
        // Fallback: list .vigthoria/proof manifests
        if (hasProof) {
            const manifests = fs.readdirSync(proofDir)
                .filter(f => f.endsWith('.json'))
                .sort()
                .reverse()
                .slice(0, 5);
            if (manifests.length > 0) {
                console.log(chalk.white('  Recent proof bundles:'));
                for (const m of manifests) {
                    try {
                        const manifest = JSON.parse(fs.readFileSync(path.join(proofDir, m), 'utf-8'));
                        const passed = manifest.previewGate?.passed ? chalk.green('passed') : chalk.red('failed');
                        console.log(chalk.gray(`    ${manifest.createdAt || m}  `) + passed + chalk.gray(`  ${manifest.entryPath || '-'}`));
                    }
                    catch {
                        console.log(chalk.gray(`    ${m}`));
                    }
                }
                console.log();
            }
        }
        else {
            console.log(chalk.gray('  No git history or proof bundles found.'));
            console.log(chalk.gray('  Run an agent task first to generate changes.'));
            console.log();
        }
    }
    /**
     * Run Template Service preview gate and persist proof bundle
     */
    async runProofGate(projectPath, captureScreenshot) {
        const spinner = createSpinner('Running preview proof gate...').start();
        try {
            const result = await this.api.runTemplateServicePreviewGate('', {
                workspacePath: projectPath,
                projectPath: projectPath,
                targetPath: projectPath,
            });
            spinner.stop();
            console.log(chalk.bold.white(`  ${CH.hLine.repeat(3)} Preview Proof Gate ${CH.hLine.repeat(39)}`));
            console.log();
            if (!result.required) {
                console.log(chalk.gray('  Preview gate: not required (no frontend artifacts detected)'));
                console.log();
                return;
            }
            const statusIcon = result.passed ? chalk.green(CH.success) : chalk.red(CH.error);
            console.log(`  ${statusIcon} Preview gate: ${result.passed ? chalk.green('PASSED') : chalk.red('FAILED')}`);
            if (result.error) {
                console.log(chalk.yellow(`  Error: ${result.error}`));
            }
            // Show modes
            if (result.modes) {
                const modes = result.modes;
                console.log();
                if (modes.design) {
                    const designStatus = modes.design.ready ? chalk.green('ready') : chalk.gray('not ready');
                    console.log(chalk.gray(`  Design mode:     `) + designStatus);
                    if (modes.design.devices) {
                        console.log(chalk.gray(`    Devices: ${modes.design.devices.join(', ')}`));
                    }
                }
                if (modes.live) {
                    const liveStatus = modes.live.ready ? chalk.green('ready') : chalk.gray('not ready');
                    console.log(chalk.gray(`  Live mode:       `) + liveStatus);
                    if (modes.live.entryPoint) {
                        console.log(chalk.gray(`    Entry: ${modes.live.entryPoint}`));
                    }
                }
                if (modes.production) {
                    const prodStatus = modes.production.ready ? chalk.green('ready') : chalk.gray('not ready');
                    console.log(chalk.gray(`  Production mode: `) + prodStatus);
                }
            }
            // Show summary
            if (result.summary) {
                console.log();
                const summary = result.summary;
                if (summary.hasViewportMeta !== undefined) {
                    console.log(chalk.gray(`  Viewport meta:    `) + (summary.hasViewportMeta ? chalk.green('yes') : chalk.yellow('missing')));
                }
                if (summary.hasResponsiveSignals !== undefined) {
                    console.log(chalk.gray(`  Responsive CSS:   `) + (summary.hasResponsiveSignals ? chalk.green('yes') : chalk.gray('no')));
                }
                if (summary.hasInteractiveSignals !== undefined) {
                    console.log(chalk.gray(`  Interactive JS:   `) + (summary.hasInteractiveSignals ? chalk.green('yes') : chalk.gray('no')));
                }
                if (typeof summary.sectionCount === 'number') {
                    console.log(chalk.gray(`  Section count:    ${summary.sectionCount}`));
                }
            }
            // Show artifacts
            if (result.artifacts) {
                console.log();
                if (result.artifacts.manifestPath) {
                    console.log(chalk.gray(`  Manifest: ${result.artifacts.manifestPath}`));
                }
                if (result.artifacts.screenshotCaptured && result.artifacts.screenshotPath) {
                    console.log(chalk.gray(`  Screenshot: ${result.artifacts.screenshotPath}`));
                }
                if (result.artifacts.previewFileUrl) {
                    console.log(chalk.gray(`  File URL: `) + chalk.cyan.underline(result.artifacts.previewFileUrl));
                }
            }
            console.log();
        }
        catch (error) {
            spinner.stop();
            this.logger.error(`Preview proof gate failed: ${error?.message || String(error)}`);
        }
    }
}
