"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveStructuralPressureScanExclusions = resolveStructuralPressureScanExclusions;
exports.scanStructuralPressureSources = scanStructuralPressureSources;
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const structural_pressure_scan_defaults_1 = require("../../config/structural-pressure-scan-defaults");
const errors_1 = require("../../errors");
const paths_1 = require("../../paths");
const workspace_discovery_1 = require("../../workspace-discovery");
const structural_pressure_thresholds_1 = require("../structural-pressure-thresholds");
const contracts_1 = require("./contracts");
function resolveStructuralPressureScanExclusions(input) {
    return {
        excludedExtensions: normalizeStructuralPressureExcludedExtensions(input?.excluded_extensions, structural_pressure_scan_defaults_1.STRUCTURAL_PRESSURE_DEFAULT_EXCLUDED_EXTENSIONS),
        blockedSegments: normalizeStructuralPressureBlockedSegments(input?.blocked_segments, structural_pressure_scan_defaults_1.STRUCTURAL_PRESSURE_DEFAULT_BLOCKED_SCAN_SEGMENTS),
        excludedBasenames: normalizeStructuralPressureExcludedBasenames(input?.excluded_basenames, structural_pressure_scan_defaults_1.STRUCTURAL_PRESSURE_DEFAULT_EXCLUDED_BASENAMES),
    };
}
function scanStructuralPressureSources(repositoryRoot, options) {
    const requestedPath = options.scanPath?.trim() ?? null;
    const roots = requestedPath === null || requestedPath === ''
        ? contracts_1.STRUCTURAL_PRESSURE_DEFAULT_SCAN_ROOTS.filter((root) => node_fs_1.default.existsSync((0, paths_1.resolveRepositoryRelativePath)(repositoryRoot, root)))
        : [normalizeScanInputPath(requestedPath)];
    const defaultRootsUsed = requestedPath === null || requestedPath === '';
    const stateByPath = new Map(options.entries.map((entry) => [entry.path, entry]));
    const files = [];
    const discovery = (0, workspace_discovery_1.discoverWorkspacePaths)({
        workspaceRoot: repositoryRoot,
        budgets: { files: 25_000, directories: 10_000, evidenceEntries: 50_000 },
        include: (entry) => entry.kind === 'file' && roots.some((root) => entry.path === root || entry.path.startsWith(`${root}/`)),
    });
    for (const root of roots) {
        const absolutePath = (0, paths_1.resolveRepositoryRelativePath)(repositoryRoot, root);
        if (!node_fs_1.default.existsSync(absolutePath)) {
            if (defaultRootsUsed) {
                continue;
            }
            throw (0, errors_1.createUsageError)(`Structural-pressure scan path not found: '${root}'.`, [
                'Pass an existing repository-relative file or directory.',
            ]);
        }
        const stat = node_fs_1.default.lstatSync(absolutePath);
        validateScanPath(repositoryRoot, root, stat.isDirectory(), options.exclusions);
        if (stat.isFile()) {
            if (discovery.paths.includes(root))
                files.push(root);
            continue;
        }
        if (stat.isDirectory()) {
            files.push(...discovery.paths.filter((filePath) => filePath.startsWith(`${root}/`) &&
                !filePath
                    .split('/')
                    .some((segment) => isBlockedStructuralPressureScanSegment(segment, options.exclusions.blockedSegments)) &&
                !isExcludedStructuralPressureScanFile(node_path_1.default.basename(filePath), options.exclusions.excludedExtensions, options.exclusions.excludedBasenames) &&
                !isBinaryFilePath(repositoryRoot, filePath)));
            continue;
        }
        throw (0, errors_1.createUsageError)(`Structural-pressure scan path '${root}' is not a regular file or directory.`, [
            'Pass a repository-relative file or directory.',
        ]);
    }
    const candidates = files
        .map((filePath) => buildStructuralPressureScanCandidate(repositoryRoot, filePath, roots, stateByPath, options.thresholds))
        .filter((candidate) => candidate !== null)
        .sort(compareStructuralPressureScanCandidates);
    const observedCandidatePaths = new Set(candidates.map((candidate) => candidate.path));
    const stateOnlyEntries = options.entries
        .filter((entry) => !observedCandidatePaths.has(entry.path))
        .map(buildStructuralPressureStateOnlyEntry)
        .sort((left, right) => left.path.localeCompare(right.path));
    return {
        summary: {
            requestedPath: defaultRootsUsed ? null : roots[0],
            defaultRootsUsed,
            roots,
            filesScanned: files.length,
            candidates: candidates.length,
            stateEntries: options.entries.length,
            trackedCandidateOverlays: candidates.filter((candidate) => candidate.reasonCodes.includes('tracked-state-entry'))
                .length,
            observedOnlyCandidates: candidates.filter((candidate) => candidate.mergeStatus === 'observed-only').length,
            trackedAndObservedCandidates: candidates.filter((candidate) => candidate.mergeStatus === 'tracked-and-observed')
                .length,
            trackedStateOnlyEntries: stateOnlyEntries.length,
        },
        candidates,
        stateOnlyEntries,
        discovery: discovery.evidence,
    };
}
function normalizeScanInputPath(input) {
    try {
        return (0, paths_1.parseRepositoryRelativePath)(input).normalizedPath;
    }
    catch (error) {
        throw (0, errors_1.createUsageError)(`Invalid structural-pressure scan path '${input}'.`, [
            error instanceof Error ? error.message : 'Pass a repository-relative file or directory path.',
            'Pass a repository-relative file or directory.',
        ]);
    }
}
function validateScanPath(repositoryRoot, filePath, isDirectory, exclusions) {
    const parsed = (0, paths_1.parseRepositoryRelativePath)(filePath);
    const blockedSegment = parsed.segments.find((segment) => isBlockedStructuralPressureScanSegment(segment, exclusions.blockedSegments));
    if (blockedSegment !== undefined) {
        throw (0, errors_1.createUsageError)(`Structural-pressure scan path '${filePath}' includes unsupported segment '${blockedSegment}'.`, [
            'Do not scan hidden repository internals, dependency directories, generated output, derived state, or planning Markdown.',
        ]);
    }
    if (!isDirectory &&
        isExcludedStructuralPressureScanFile(filePath, exclusions.excludedExtensions, exclusions.excludedBasenames)) {
        throw (0, errors_1.createUsageError)(`Structural-pressure scan path '${filePath}' uses an unsupported file extension.`, [
            `Excluded extensions: ${[...new Set(exclusions.excludedExtensions)].sort().join(', ')}.`,
        ]);
    }
    if (!isDirectory && isBinaryFilePath(repositoryRoot, filePath)) {
        throw (0, errors_1.createUsageError)(`Structural-pressure scan path '${filePath}' appears to be a binary file.`, [
            'Use --path with a text source file or directory.',
        ]);
    }
}
function isExcludedStructuralPressureScanFile(filePath, excludedExtensions, excludedBasenames) {
    const basename = node_path_1.default.posix.basename(filePath).toLowerCase();
    const extension = node_path_1.default.extname(basename);
    const excludedSet = new Set(excludedExtensions.map((value) => value.toLowerCase()));
    const excludedBasenameSet = new Set(excludedBasenames.map((value) => value.toLowerCase()));
    if (excludedBasenameSet.has(basename)) {
        return true;
    }
    if (excludedSet.has(basename)) {
        return true;
    }
    if (excludedSet.has(extension)) {
        return true;
    }
    return basename.endsWith('.min.js') || basename.endsWith('.min.css');
}
function isBinaryFilePath(repositoryRoot, filePath) {
    const absolutePath = (0, paths_1.resolveRepositoryRelativePath)(repositoryRoot, filePath);
    const maxBytesToInspect = 4096;
    const buffer = Buffer.allocUnsafe(maxBytesToInspect);
    const fileDescriptor = node_fs_1.default.openSync(absolutePath, 'r');
    try {
        const bytesRead = node_fs_1.default.readSync(fileDescriptor, buffer, 0, maxBytesToInspect, 0);
        if (bytesRead === 0) {
            return false;
        }
        let nonTextBytes = 0;
        for (let index = 0; index < bytesRead; index += 1) {
            const value = buffer[index];
            if (value === 0) {
                return true;
            }
            // Allow common printable ASCII and whitespace control bytes.
            const isWhitespaceControl = value === 9 || value === 10 || value === 13;
            const isPrintableAscii = value >= 32 && value <= 126;
            if (!isWhitespaceControl && !isPrintableAscii) {
                nonTextBytes += 1;
            }
        }
        return nonTextBytes / bytesRead > 0.3;
    }
    finally {
        node_fs_1.default.closeSync(fileDescriptor);
    }
}
function buildStructuralPressureScanCandidate(repositoryRoot, filePath, roots, stateByPath, thresholds) {
    const absolutePath = (0, paths_1.resolveRepositoryRelativePath)(repositoryRoot, filePath);
    const fileKind = classifyStructuralPressureFileKind(filePath);
    const threshold = fileKind === 'test'
        ? (0, structural_pressure_thresholds_1.structuralPressureThresholdValues)(thresholds.test)
        : (0, structural_pressure_thresholds_1.structuralPressureThresholdValues)(thresholds.source);
    const lineCount = countFileLines(node_fs_1.default.readFileSync(absolutePath, 'utf8'));
    const classification = classifyStructuralPressureLineCount(lineCount, threshold);
    const trackedEntry = stateByPath.get(filePath);
    const trackedState = trackedEntry === undefined ? undefined : buildStructuralPressureTrackedStateMetadata(trackedEntry);
    if (classification === null && trackedEntry === undefined) {
        return null;
    }
    const reasonCodes = [fileKind === 'test' ? 'large-test-file' : 'large-source-file'];
    if (trackedEntry !== undefined) {
        reasonCodes.push('tracked-state-entry');
    }
    return {
        path: filePath,
        mergeStatus: trackedEntry === undefined ? 'observed-only' : 'tracked-and-observed',
        requestedPathContext: findRequestedPathContext(filePath, roots),
        fileKind,
        lineCount,
        threshold,
        classification: classification ?? 'watch',
        sourceType: 'source-scan',
        reasonCodes,
        ...(trackedEntry === undefined
            ? {}
            : {
                trackedStatus: trackedEntry.status,
                trackedSeverity: trackedEntry.severity,
                trackedSummary: trackedEntry.summary,
                trackedEvidencePaths: trackedEntry.evidence_paths,
                trackedState,
            }),
    };
}
function buildStructuralPressureStateOnlyEntry(entry) {
    return {
        path: entry.path,
        mergeStatus: 'tracked-state-only',
        sourceType: 'derived-state',
        reasonCodes: ['tracked-state-entry'],
        trackedState: buildStructuralPressureTrackedStateMetadata(entry),
    };
}
function buildStructuralPressureTrackedStateMetadata(entry) {
    return {
        status: entry.status,
        severity: entry.severity,
        summary: entry.summary,
        evidencePaths: entry.evidence_paths,
        sourceMilestoneReconciliationPath: entry.source_milestone_reconciliation,
        sourceMilestoneOverviewPath: entry.source_milestone_overview,
        ...(entry.structural_delta === undefined ? {} : { structuralDelta: entry.structural_delta }),
    };
}
function classifyStructuralPressureFileKind(filePath) {
    const segments = (0, paths_1.parseRepositoryRelativePath)(filePath).segments;
    const basename = node_path_1.default.posix.basename(filePath).toLowerCase();
    if (contracts_1.STRUCTURAL_PRESSURE_TEST_ROOTS.some((root) => segments[0] === root) ||
        segments.includes('__tests__') ||
        /\.(?:suite|test|spec)\.[cm]?[jt]sx?$/.test(basename)) {
        return 'test';
    }
    return 'source';
}
function classifyStructuralPressureLineCount(lineCount, threshold) {
    if (lineCount >= threshold.hotspot) {
        return 'hotspot';
    }
    if (lineCount >= threshold.pressure) {
        return 'pressure';
    }
    if (lineCount >= threshold.watch) {
        return 'watch';
    }
    return null;
}
function countFileLines(text) {
    if (text.length === 0) {
        return 0;
    }
    return text.endsWith('\n') ? text.split(/\r?\n/).length - 1 : text.split(/\r?\n/).length;
}
function findRequestedPathContext(filePath, roots) {
    return roots.find((root) => filePath === root || filePath.startsWith(`${root}/`)) ?? filePath;
}
function isBlockedStructuralPressureScanSegment(segment, blockedSegments) {
    const blockedSet = new Set(blockedSegments.map((value) => value.toLowerCase()));
    return segment.startsWith('.') || blockedSet.has(segment.toLowerCase());
}
function normalizeStructuralPressureExcludedExtensions(configured, fallback) {
    const source = configured ?? fallback;
    return [...new Set(source.map((entry) => entry.trim().toLowerCase()))]
        .filter((entry) => entry.length > 0)
        .filter((entry) => entry.startsWith('.'))
        .sort();
}
function normalizeStructuralPressureBlockedSegments(configured, fallback) {
    const source = configured ?? fallback;
    return [...new Set(source.map((entry) => entry.trim().toLowerCase()))]
        .filter((entry) => entry.length > 0)
        .filter((entry) => !entry.includes('/') && !entry.includes('\\'))
        .sort();
}
function normalizeStructuralPressureExcludedBasenames(configured, fallback) {
    const source = configured ?? fallback;
    return [...new Set(source.map((entry) => entry.trim().toLowerCase()))]
        .filter((entry) => entry.length > 0)
        .filter((entry) => !entry.includes('/') && !entry.includes('\\'))
        .sort();
}
function compareStructuralPressureScanCandidates(left, right) {
    return (classificationSortRank(right.classification) - classificationSortRank(left.classification) ||
        left.path.localeCompare(right.path));
}
function classificationSortRank(classification) {
    switch (classification) {
        case 'hotspot':
            return 3;
        case 'pressure':
            return 2;
        case 'watch':
            return 1;
    }
}
