"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TidyService = void 0;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const chalk_1 = __importDefault(require("chalk"));
const tokenParser_1 = require("../utils/tokenParser");
const tailwindSemantic_1 = require("../utils/tailwindSemantic");
const ProjectScanner_1 = require("./ProjectScanner");
const SkeletonService_1 = require("./SkeletonService");
class TidyService {
    constructor(projectRoot, excludePatterns = []) {
        this.projectRoot = projectRoot;
        this.tokens = [];
        this.constants = [];
        /** Semantic color names from globals/theme CSS (bg-*, text-*, border-*). */
        this.themeColorNames = new Set();
        this.FUZZY_THRESHOLD = 15; // JND (Just Noticeable Difference) in RGB space is ~10-20
        this.tokenDefinitionFiles = new Set(); // Track files that DEFINE tokens
        this.constantDefinitionFiles = new Set(); // Track files that DEFINE constants
        this.excludePatterns = []; // Custom exclusion patterns
        this.excludePatterns = excludePatterns;
    }
    /**
     * Initialize symbols (tokens and constants)
     */
    async initialize() {
        try {
            // 1. Find potential token files
            const scanner = new ProjectScanner_1.ProjectScanner(this.projectRoot);
            const snapshot = await scanner.scan();
            // Look for globals.css, theme.css, variables.css
            const cssFiles = snapshot.fileTree.filter(f => f.path.endsWith(".css") &&
                (f.path.includes("globals") || f.path.includes("theme") || f.path.includes("variables")));
            for (const file of cssFiles) {
                const absCss = path.join(this.projectRoot, file.path);
                const parsed = await tokenParser_1.TokenParser.parseCssVariables(absCss);
                this.tokens.push(...parsed);
                const rawCss = await fs.readFile(absCss, "utf8");
                (0, tailwindSemantic_1.parseThemeColorNamesFromCss)(rawCss).forEach((n) => this.themeColorNames.add(n));
                // Mark this file as a token definition file - should NOT be scanned for replacements
                this.tokenDefinitionFiles.add(file.path);
            }
            // Look for constants.ts, constants.js, branding.ts, branding.js
            const constantFiles = snapshot.fileTree.filter(f => (f.path.endsWith(".ts") || f.path.endsWith(".js")) &&
                (f.path.includes("constants") || f.path.includes("branding")));
            for (const file of constantFiles) {
                const parsed = await tokenParser_1.TokenParser.parseConstants(path.join(this.projectRoot, file.path));
                this.constants.push(...parsed);
                // Mark this file as a constant definition file - should NOT be scanned for replacements
                this.constantDefinitionFiles.add(file.path);
            }
        }
        catch (e) {
            console.error(chalk_1.default.red(`[TidyService] Initialization failed: ${e}`));
        }
    }
    /**
     * Check if a file should be excluded from tidy scanning
     */
    shouldExcludeFile(relativeFilePath) {
        // 1. Check if it's a token/constant definition file
        if (this.tokenDefinitionFiles.has(relativeFilePath) ||
            this.constantDefinitionFiles.has(relativeFilePath)) {
            return true;
        }
        // 2. Check against custom exclusion patterns
        for (const pattern of this.excludePatterns) {
            if (relativeFilePath.includes(pattern)) {
                return true;
            }
        }
        // 3. Additional hardcoded exclusions for common design system files
        const designSystemPatterns = [
            'design-tokens',
            'tokens/',
            'tailwind.config',
            'theme.config',
            'app.css',
            'index.css',
            'styles/tokens',
            'styles/variables'
        ];
        for (const pattern of designSystemPatterns) {
            if (relativeFilePath.includes(pattern)) {
                return true;
            }
        }
        return false;
    }
    /**
     * Audit the project for hardcoded values
     */
    async audit(targetDirs, auditOpts) {
        const opts = {
            hexTokens: auditOpts?.hexTokens !== false,
            constants: auditOpts?.constants !== false,
            semanticTailwind: auditOpts?.semanticTailwind !== false,
            darkContrast: auditOpts?.darkContrast !== false,
        };
        const suggestions = [];
        const filesToScan = [];
        // Search for relevant files
        let actualTargetDirs = targetDirs.filter(d => fs.pathExistsSync(path.join(this.projectRoot, d)));
        // If none of the standard dirs exist, default to the root itself
        if (actualTargetDirs.length === 0) {
            actualTargetDirs = ["."];
        }
        const ignoredNames = new Set(["node_modules", ".next", "dist", ".git", ".turbo", "out"]);
        for (const dir of actualTargetDirs) {
            const absDir = path.join(this.projectRoot, dir);
            if (!(await fs.pathExists(absDir)))
                continue;
            const walk = async (currentPath) => {
                const entries = await fs.readdir(currentPath, { withFileTypes: true });
                for (const entry of entries) {
                    const fullPath = path.join(currentPath, entry.name);
                    if (entry.isDirectory()) {
                        if (!ignoredNames.has(entry.name)) {
                            await walk(fullPath);
                        }
                    }
                    else if (/\.(tsx|jsx|ts|js|css)$/.test(entry.name)) {
                        filesToScan.push(fullPath);
                    }
                }
            };
            await walk(absDir);
        }
        for (const file of filesToScan) {
            const relativeFilePath = path.relative(this.projectRoot, file);
            // Skip files that define tokens/constants (would create circular references)
            if (this.shouldExcludeFile(relativeFilePath)) {
                continue;
            }
            const content = await fs.readFile(file, "utf8");
            const lines = content.split("\n");
            for (let i = 0; i < lines.length; i++) {
                const line = lines[i] || "";
                // 1. Check for color literals (Hex)
                if (opts.hexTokens) {
                    const hexRegex = /#(?:[0-9a-fA-F]{3}){1,2}/g;
                    let match;
                    while ((match = hexRegex.exec(line)) !== null) {
                        // Check if this hex is part of a variable definition (preceded by --name:)
                        const beforeMatch = line.substring(0, match.index).trim();
                        if (beforeMatch.endsWith(":") && beforeMatch.includes("--")) {
                            continue;
                        }
                        const literal = match[0];
                        const bestToken = this.findBestTokenMatch(literal);
                        if (bestToken) {
                            // Validate: Don't suggest replacing a value with a var that references itself
                            const wouldCreateCircular = this.wouldCreateCircularReference(relativeFilePath, literal, bestToken.name);
                            if (!wouldCreateCircular) {
                                // Calculate context-aware confidence
                                const confidence = this.calculateConfidence(bestToken.distance, relativeFilePath, bestToken.sourceFile);
                                suggestions.push({
                                    file: relativeFilePath,
                                    line: i + 1,
                                    original: literal,
                                    replacement: `var(${bestToken.name})`,
                                    tokenName: bestToken.name,
                                    type: "token",
                                    confidence
                                });
                            }
                        }
                        else if (literal.length >= 4) {
                            suggestions.push({
                                file: path.relative(this.projectRoot, file),
                                line: i + 1,
                                original: literal,
                                replacement: "NEW_TOKEN",
                                type: "new_token",
                                confidence: 0.5
                            });
                        }
                    }
                }
                // 2. Check for string literal matches against constants
                if (opts.constants) {
                    for (const constant of this.constants) {
                        if (constant.value.length < 3)
                            continue;
                        if (line.includes(`"${constant.value}"`) || line.includes(`'${constant.value}'`)) {
                            if (file.includes(constant.sourceFile))
                                continue;
                            suggestions.push({
                                file: path.relative(this.projectRoot, file),
                                line: i + 1,
                                original: constant.value,
                                replacement: constant.name,
                                tokenName: constant.name,
                                type: "constant",
                                confidence: 0.95
                            });
                        }
                    }
                }
            }
            // 3. Tailwind palette → semantic (shadcn) utilities in class strings
            if (opts.semanticTailwind && this.themeColorNames.size > 0) {
                this.collectTailwindSemanticSuggestions(lines, relativeFilePath, file, suggestions, opts.darkContrast);
            }
        }
        return this.dedupeSuggestions(suggestions);
    }
    /**
     * Suggest replacing default palette utilities (e.g. bg-white) with theme semantic classes.
     */
    collectTailwindSemanticSuggestions(lines, relativeFilePath, filePath, suggestions, darkContrast) {
        const ext = path.extname(filePath);
        if (![".tsx", ".jsx", ".ts", ".js"].includes(ext))
            return;
        for (let i = 0; i < lines.length; i++) {
            const line = lines[i] || "";
            const chunks = (0, tailwindSemantic_1.extractClassListStringsFromLine)(line);
            const occurrenceCount = new Map();
            for (const chunk of chunks) {
                const tokens = (0, tailwindSemantic_1.splitClassTokens)(chunk);
                for (const token of tokens) {
                    const mapped = (0, tailwindSemantic_1.mapPaletteUtilityToSemantic)(token, this.themeColorNames);
                    if (mapped.kind === "none")
                        continue;
                    const occ = occurrenceCount.get(token) ?? 0;
                    occurrenceCount.set(token, occ + 1);
                    if (mapped.kind === "replace") {
                        suggestions.push({
                            file: relativeFilePath,
                            line: i + 1,
                            original: token,
                            replacement: mapped.replacement,
                            type: "tailwind_semantic",
                            confidence: mapped.confidence,
                            occurrenceIndex: occ,
                        });
                    }
                    else if (mapped.kind === "ambiguous") {
                        suggestions.push({
                            file: relativeFilePath,
                            line: i + 1,
                            original: token,
                            replacement: mapped.candidates[0] ?? "",
                            type: "tailwind_semantic",
                            confidence: mapped.confidence,
                            occurrenceIndex: occ,
                            ambiguousCandidates: mapped.candidates,
                        });
                    }
                }
                const contrastIssues = (0, tailwindSemantic_1.findContrastAndPairingIssues)(tokens, this.themeColorNames);
                for (const issue of contrastIssues) {
                    const occ = occurrenceCount.get(issue.original) ?? 0;
                    occurrenceCount.set(issue.original, occ + 1);
                    suggestions.push({
                        file: relativeFilePath,
                        line: i + 1,
                        original: issue.original,
                        replacement: issue.replacement,
                        type: "tailwind_semantic",
                        confidence: issue.confidence,
                        occurrenceIndex: occ,
                        pairedWith: issue.pairedWith,
                        rationale: issue.rationale,
                        action: "rewrite",
                    });
                }
                if (darkContrast) {
                    const darkIssues = (0, tailwindSemantic_1.findDarkContrastNormalizationIssues)(tokens, this.themeColorNames);
                    for (const issue of darkIssues) {
                        const occ = occurrenceCount.get(issue.original) ?? 0;
                        occurrenceCount.set(issue.original, occ + 1);
                        suggestions.push({
                            file: relativeFilePath,
                            line: i + 1,
                            original: issue.original,
                            replacement: issue.replacement,
                            type: "tailwind_semantic",
                            confidence: issue.confidence,
                            occurrenceIndex: occ,
                            rationale: issue.rationale,
                            action: issue.action,
                            appendClass: issue.appendClass,
                        });
                    }
                }
            }
        }
    }
    /**
     * Drop duplicate hex suggestions on the same line (one apply replaces all occurrences).
     */
    dedupeSuggestions(suggestions) {
        const byKey = new Map();
        for (const s of suggestions) {
            const key = s.type === "tailwind_semantic"
                ? `${s.file}:${s.line}:${s.original}:${s.occurrenceIndex ?? 0}:tailwind_semantic`
                : `${s.file}:${s.line}:${s.original}:${s.type}`;
            const existing = byKey.get(key);
            if (!existing) {
                byKey.set(key, s);
                continue;
            }
            if (s.type === "tailwind_semantic" && existing.type === "tailwind_semantic") {
                const existingScore = (existing.rationale ? 2 : 0) + (existing.action ? 1 : 0);
                const incomingScore = (s.rationale ? 2 : 0) + (s.action ? 1 : 0);
                if (incomingScore > existingScore) {
                    byKey.set(key, s);
                }
            }
        }
        return [...byKey.values()];
    }
    /**
     * Apply a suggestion to the actual file
     */
    async applySuggestion(suggestion) {
        try {
            const fullPath = path.join(this.projectRoot, suggestion.file);
            if (!(await fs.pathExists(fullPath)))
                return false;
            const content = await fs.readFile(fullPath, "utf8");
            const lines = content.split("\n");
            const lineIndex = suggestion.line - 1;
            if (lineIndex < 0 || lineIndex >= lines.length)
                return false;
            const line = lines[lineIndex];
            if (line === undefined)
                return false;
            let newLine = line;
            if (suggestion.type === "token" || suggestion.type === "new_token") {
                // use regex with boundary to avoid partial matches
                const escaped = suggestion.original.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                // If it's a new token, the replacement should be the var(--NAME) form
                const replacement = suggestion.type === "new_token" && suggestion.tokenName
                    ? `var(--${suggestion.tokenName})`
                    : suggestion.replacement;
                newLine = line.replace(new RegExp(escaped, 'g'), replacement);
            }
            else if (suggestion.type === "constant") {
                newLine = line.replace(`"${suggestion.original}"`, suggestion.replacement);
                newLine = newLine.replace(`'${suggestion.original}'`, suggestion.replacement);
            }
            else if (suggestion.type === "tailwind_semantic") {
                const occ = suggestion.occurrenceIndex ?? 0;
                const action = suggestion.action ?? "rewrite";
                const appendClass = suggestion.appendClass;
                if (action === "append" && appendClass) {
                    if (line.includes(appendClass)) {
                        return true;
                    }
                    const replaced = (0, tailwindSemantic_1.replaceNthClassOccurrence)(line, suggestion.original, `${suggestion.original} ${appendClass}`, occ);
                    if (replaced === null) {
                        return false;
                    }
                    newLine = replaced;
                }
                else if (action === "append_and_rewrite" && appendClass) {
                    const combinedReplacement = line.includes(appendClass)
                        ? suggestion.replacement
                        : `${suggestion.replacement} ${appendClass}`;
                    const replaced = (0, tailwindSemantic_1.replaceNthClassOccurrence)(line, suggestion.original, combinedReplacement, occ);
                    if (replaced === null) {
                        if (!line.includes(suggestion.original) && line.includes(suggestion.replacement)) {
                            return true;
                        }
                        return false;
                    }
                    newLine = replaced;
                }
                else {
                    const replaced = (0, tailwindSemantic_1.replaceNthClassOccurrence)(line, suggestion.original, suggestion.replacement, occ);
                    if (replaced === null) {
                        if (line.includes(suggestion.replacement) && !line.includes(suggestion.original)) {
                            return true;
                        }
                        return false;
                    }
                    newLine = replaced;
                }
            }
            if (newLine === line) {
                // Handle cases where a previous replacement on the same line already fixed this
                const replacementStr = suggestion.type === "new_token" && suggestion.tokenName
                    ? `var(--${suggestion.tokenName})`
                    : suggestion.replacement;
                if (suggestion.type === "tailwind_semantic") {
                    if (!line.includes(suggestion.original) &&
                        line.includes(suggestion.replacement)) {
                        return true;
                    }
                }
                else if (line.includes(replacementStr)) {
                    return true;
                }
                return false;
            }
            lines[lineIndex] = newLine;
            await fs.writeFile(fullPath, lines.join("\n"));
            return true;
        }
        catch (e) {
            console.error(chalk_1.default.red(`[TidyService] Failed to apply suggestion in ${suggestion.file}:`), e);
            return false;
        }
    }
    /**
     * Append a new token or constant to the source file
     */
    async appendNewToken(name, value, type) {
        try {
            const scanner = new ProjectScanner_1.ProjectScanner(this.projectRoot);
            const snapshot = await scanner.scan();
            if (type === "token") {
                const cssFile = snapshot.fileTree.find(f => f.path.endsWith("globals.css")) ||
                    snapshot.fileTree.find(f => f.path.endsWith(".css"));
                if (!cssFile)
                    return false;
                const absPath = path.join(this.projectRoot, cssFile.path);
                const content = await fs.readFile(absPath, "utf8");
                // Idempotency: don't add if already exists
                if (content.includes(`--${name}:`))
                    return true;
                const cleanValue = value.includes("var(--") ? value.replace(/var\(--|\)/g, "") : value;
                const rootRegex = /:root\s*{([^}]*)}/;
                const match = rootRegex.exec(content);
                if (match && match[0] && match[1] !== undefined) {
                    // Add newline before token only (prevents double spacing)
                    const newContent = content.replace(match[0], match[0].replace(match[1], match[1] + `\n  --${name}: ${cleanValue};`));
                    await fs.writeFile(absPath, newContent);
                    return true;
                }
            }
            else if (type === "constant") {
                const tsFile = snapshot.fileTree.find(f => f.path.includes("constants" + ".ts")) ||
                    snapshot.fileTree.find(f => f.path.includes("constants" + ".js"));
                if (!tsFile)
                    return false;
                const absPath = path.join(this.projectRoot, tsFile.path);
                const content = await fs.readFile(absPath, "utf8");
                const newDeclaration = `\nexport const ${name} = "${value}";`;
                await fs.writeFile(absPath, content + newDeclaration);
                return true;
            }
            return false;
        }
        catch (e) {
            console.error(chalk_1.default.red(`[TidyService] Failed to append new ${type}:`), e);
            return false;
        }
    }
    async auditSkeletons() {
        const skeletonService = new SkeletonService_1.SkeletonService(this.projectRoot);
        return skeletonService.auditSkeletons();
    }
    async applySkeleton(suggestion) {
        const skeletonService = new SkeletonService_1.SkeletonService(this.projectRoot);
        await skeletonService.ensureSkeletonComponent();
        return skeletonService.applySuggestion(suggestion);
    }
    /**
     * Calculate confidence score based on color distance and file context
     */
    calculateConfidence(colorDistance, targetFile, tokenSourceFile) {
        let confidence = 1.0;
        // Base confidence on color distance
        if (colorDistance === 0) {
            confidence = 1.0; // Perfect match
        }
        else if (colorDistance <= 5) {
            confidence = 0.95; // Very close
        }
        else if (colorDistance <= 10) {
            confidence = 0.85; // Close
        }
        else {
            confidence = 0.7; // Within threshold but not very close
        }
        // Reduce confidence if in same directory as token definition
        // (might be intentionally using similar but different colors)
        if (tokenSourceFile) {
            const targetDir = path.dirname(targetFile);
            const sourceDir = path.dirname(tokenSourceFile);
            if (targetDir === sourceDir) {
                confidence *= 0.9; // 10% penalty for same directory
            }
        }
        return confidence;
    }
    /**
     * Check if replacing a literal would create a circular reference
     * (e.g., suggesting to replace #06C807 with var(--traffic-green) in the file
     * where --traffic-green is defined as #06C807)
     */
    wouldCreateCircularReference(filePath, literal, tokenName) {
        // Find the token that would be used as replacement
        const token = this.tokens.find(t => t.name === tokenName);
        if (!token)
            return false;
        // If the token's source file is the same as the file being scanned,
        // and the token's value matches the literal, it would be circular
        if (token.sourceFile && filePath.includes(token.sourceFile)) {
            // Check if the token value matches the literal (exact or fuzzy match)
            const normalizedTokenValue = token.value.toLowerCase().replace(/\s/g, '');
            const normalizedLiteral = literal.toLowerCase().replace(/\s/g, '');
            if (normalizedTokenValue === normalizedLiteral) {
                return true;
            }
        }
        return false;
    }
    findBestTokenMatch(literal) {
        let bestMatch = null;
        let minDistance = Infinity;
        for (const token of this.tokens) {
            if (token.type !== "color")
                continue;
            const dist = tokenParser_1.TokenParser.getColorDistance(literal, token.value);
            if (dist < minDistance && dist <= this.FUZZY_THRESHOLD) {
                minDistance = dist;
                bestMatch = { ...token, distance: dist };
            }
        }
        return bestMatch;
    }
    /**
     * Remove circular references and duplicates from CSS files
     * WARNING: This should NOT run on design system definition files!
     */
    async cleanupCircularTokens() {
        let removedCount = 0;
        try {
            const scanner = new ProjectScanner_1.ProjectScanner(this.projectRoot);
            const snapshot = await scanner.scan();
            const cssFiles = snapshot.fileTree.filter(f => f.path.endsWith(".css"));
            for (const file of cssFiles) {
                // CRITICAL: Skip design system files - they may have scoped overrides
                // (e.g., :root vs .dark) that look like duplicates but aren't!
                if (this.shouldExcludeFile(file.path)) {
                    continue;
                }
                const absPath = path.join(this.projectRoot, file.path);
                const content = await fs.readFile(absPath, "utf8");
                let newContent = content;
                // 1. Surgical removal of circular tokens ONLY (preserves '}' on same line)
                const circularRegex = /--([\w-]+):\s*var\(--\1\);?\s*/g;
                newContent = newContent.replace(circularRegex, () => {
                    removedCount++;
                    return "";
                });
                // 2. DISABLED: Deduplication is too dangerous for CSS
                // It can't distinguish between:
                //   :root { --bg: white; }
                //   .dark { --bg: black; }  <-- Not a duplicate, it's a scoped override!
                //
                // We only remove true circular references (--var: var(--var))
                if (newContent !== content) {
                    await fs.writeFile(absPath, newContent);
                }
            }
        }
        catch (e) {
            console.error(chalk_1.default.red(`[TidyService] Cleanup failed: ${e}`));
        }
        return removedCount;
    }
}
exports.TidyService = TidyService;
//# sourceMappingURL=TidyService.js.map