import * as fs from 'fs-extra';
import * as path from 'path';
import { ts } from 'ts-morph';
import { logger } from './logger';

const { glob } = require('tinyglobby');

/**
 * Result of parsing API markdown exports
 */
export interface ApiMarkdownExports {
    // Map of symbol name to the source file paths where it's exported
    symbolToFiles: Map<string, Set<string>>;
    // Set of all relevant *.api.md files found
    apiMdFiles: Set<string>;
}

/**
 * Utility class for parsing *.api.md files generated by API Extractor
 */
export class ApiMarkdownParser {
    private sourceRoot: string;
    private symbolToFiles: Map<string, Set<string>>;
    private apiMdFiles: Set<string>;

    constructor(sourceRoot: string) {
        this.sourceRoot = path.resolve(sourceRoot);
        this.symbolToFiles = new Map<string, Set<string>>();
        this.apiMdFiles = new Set<string>();
    }

    /**
     * Parse all *.api.md files in the source root and extract exported symbols
     */
    public async parseApiMarkdownFiles(): Promise<ApiMarkdownExports> {
        logger.info(`Scanning for *.api.md files in ${this.sourceRoot}`);

        if (!fs.existsSync(this.sourceRoot)) {
            logger.error(`Source root path does not exist: ${this.sourceRoot}`);
            return {
                symbolToFiles: this.symbolToFiles,
                apiMdFiles: this.apiMdFiles
            };
        }

        // Find all *.api.md files recursively
        const pattern = path.join(this.sourceRoot, '**/*.api.md');
        const apiMdFiles = await glob(pattern, {
            absolute: true,
            ignore: ['**/node_modules/**']
        });

        logger.info(`Found ${apiMdFiles.length} *.api.md file(s)`);

        // Process each *.api.md file
        for (const apiMdFile of apiMdFiles) {
            // Check if the file is relevant before adding it
            if (await this.isRelevantApiMarkdownFile(apiMdFile)) {
                this.apiMdFiles.add(apiMdFile);
                await this.parseApiMarkdownFile(apiMdFile);
            } else {
                logger.debug(`Skipping irrelevant API markdown file: ${apiMdFile}`);
            }
        }

        logger.info(`Extracted ${this.symbolToFiles.size} public API symbol(s) from ${this.apiMdFiles.size} relevant *.api.md file(s)`);

        return {
            symbolToFiles: this.symbolToFiles,
            apiMdFiles: this.apiMdFiles
        };
    }

    /**
     * Check if an API markdown file is relevant (contains meaningful exports)
     * A file is relevant if it contains at least one export that is:
     * - A class, interface, function, type, enum, or variable
     * - Not a default export of an empty object
     * - Not an Angular internal symbol (ɵfac, ɵinj, ɵmod)
     */
    private async isRelevantApiMarkdownFile(filePath: string): Promise<boolean> {
        try {
            const content = fs.readFileSync(filePath, 'utf-8');
            
            // Extract the TypeScript code block from the markdown file
            const tsCodeBlock = this.extractTypeScriptCodeBlock(content);
            
            if (!tsCodeBlock) {
                logger.debug(`No TypeScript code block found in ${filePath}`);
                return false;
            }

            // Parse the TypeScript code to check for meaningful exports
            const sourceFile = ts.createSourceFile(
                filePath,
                tsCodeBlock,
                ts.ScriptTarget.Latest,
                true
            );

            // Check each top-level statement
            for (const statement of sourceFile.statements) {
                if (this.isMeaningfulExport(statement)) {
                    return true;
                }
            }

            return false;
        } catch (error) {
            logger.error(`Error checking relevance of ${filePath}: ${error}`);
            return false;
        }
    }

    /**
     * Check if a statement is a meaningful export
     * Exclude default exports of empty objects and Angular internal symbols
     */
    private isMeaningfulExport(statement: ts.Node): boolean {
        // Handle: export class Foo {}
        if (ts.isClassDeclaration(statement)) {
            const name = (statement as any).name?.text;
            return !!name && !this.isAngularInternalSymbol(name);
        }
        
        // Handle: export interface Bar {}
        if (ts.isInterfaceDeclaration(statement)) {
            const name = (statement as any).name?.text;
            return !!name;
        }
        
        // Handle: export const baz = ...
        if (ts.isVariableStatement(statement)) {
            const hasExportModifier = (statement as any).modifiers?.some(
                (m: ts.Modifier) => m.kind === ts.SyntaxKind.ExportKeyword
            );
            if (hasExportModifier) {
                // Check if it's not just a default export of an empty object
                for (const decl of (statement as any).declarationList?.declarations || []) {
                    const name = decl.name?.text;
                    if (name && !this.isAngularInternalSymbol(name) && name !== '_default') {
                        return true;
                    }
                }
            }
            return false;
        }
        
        // Handle: export function foo() {}
        if (ts.isFunctionDeclaration(statement)) {
            const name = (statement as any).name?.text;
            return !!name && !this.isAngularInternalSymbol(name);
        }
        
        // Handle: export type FooType = ...
        if (ts.isTypeAliasDeclaration(statement)) {
            const name = (statement as any).name?.text;
            return !!name;
        }
        
        // Handle: export enum FooEnum {}
        if (ts.isEnumDeclaration(statement)) {
            const name = (statement as any).name?.text;
            return !!name;
        }
        
        // Handle: export { Foo, Bar }
        if (ts.isExportDeclaration(statement)) {
            const exportClause = (statement as any).exportClause;
            
            // Skip: export default _default
            if ((statement as any).isTypeOnly) {
                return false;
            }
            
            // Check if it's a named export declaration
            if (exportClause && ts.isNamedExports(exportClause)) {
                const elements = exportClause.elements;
                for (const element of elements) {
                    const name = element.name?.text;
                    if (name && !this.isAngularInternalSymbol(name) && name !== '_default') {
                        return true;
                    }
                }
            }
            
            return false;
        }
        
        // Handle: export default Foo
        if (ts.isExportAssignment(statement)) {
            // export default _default is not meaningful
            if (ts.isIdentifier((statement as any).expression)) {
                const name = (statement as any).expression.text;
                if (name === '_default') {
                    return false;
                }
                return !this.isAngularInternalSymbol(name);
            }
            return false;
        }

        return false;
    }

    /**
     * Check if a symbol is an Angular internal symbol
     */
    private isAngularInternalSymbol(symbolName: string): boolean {
        // Angular internal symbols start with ɵ
        return symbolName.startsWith('ɵ');
    }

    /**
     * Extract the TypeScript code block from a markdown file
     * API Extractor markdown files have TypeScript code in a ```ts block
     *
     * Note: Uses string-based parsing instead of regex to avoid ReDoS vulnerabilities
     * that can occur with patterns like /```ts\s*\n([\s\S]*?)\n```/ on malformed input.
     */
    private extractTypeScriptCodeBlock(content: string): string | null {
        // Find the opening marker (```ts or ```typescript)
        const openMarkers = ['```ts\n', '```ts\r\n', '```typescript\n', '```typescript\r\n'];
        let startIndex = -1;
        let markerLength = 0;

        for (const marker of openMarkers) {
            const idx = content.indexOf(marker);
            if (idx !== -1 && (startIndex === -1 || idx < startIndex)) {
                startIndex = idx;
                markerLength = marker.length;
            }
        }

        if (startIndex === -1) {
            return null;
        }

        // Find the closing marker (``` at the start of a line)
        const contentStart = startIndex + markerLength;
        const closeMarker = '\n```';
        const endIndex = content.indexOf(closeMarker, contentStart);

        if (endIndex === -1) {
            return null;
        }

        const codeBlock = content.slice(contentStart, endIndex);

        // Return null for empty code blocks
        if (!codeBlock.trim()) {
            return null;
        }

        return codeBlock;
    }

    /**
     * Parse a single *.api.md file and extract its exports
     */
    private async parseApiMarkdownFile(filePath: string): Promise<void> {
        logger.debug(`Parsing API markdown file: ${filePath}`);

        try {
            const content = fs.readFileSync(filePath, 'utf-8');
            const tsCodeBlock = this.extractTypeScriptCodeBlock(content);
            
            if (!tsCodeBlock) {
                logger.debug(`No TypeScript code block found in ${filePath}`);
                return;
            }

            const sourceFile = ts.createSourceFile(
                filePath,
                tsCodeBlock,
                ts.ScriptTarget.Latest,
                true
            );

            // Process all top-level statements in the file
            for (const statement of sourceFile.statements) {
                this.processStatement(statement, filePath);
            }
        } catch (error) {
            logger.error(`Error parsing API markdown file ${filePath}: ${error}`);
        }
    }

    /**
     * Process a TypeScript statement to extract exports
     */
    private processStatement(statement: ts.Node, sourceFilePath: string): void {
        // Handle: export class Foo {}
        if (ts.isClassDeclaration(statement)) {
            const name = (statement as any).name?.text;
            if (name && !this.isAngularInternalSymbol(name)) {
                this.addSymbol(name, sourceFilePath);
            }
        }
        // Handle: export interface Bar {}
        else if (ts.isInterfaceDeclaration(statement)) {
            const name = (statement as any).name?.text;
            if (name) {
                this.addSymbol(name, sourceFilePath);
            }
        }
        // Handle: export const baz = ...
        else if (ts.isVariableStatement(statement)) {
            const hasExportModifier = (statement as any).modifiers?.some(
                (m: ts.Modifier) => m.kind === ts.SyntaxKind.ExportKeyword
            );
            if (hasExportModifier) {
                for (const decl of (statement as any).declarationList?.declarations || []) {
                    const name = decl.name?.text;
                    if (name && !this.isAngularInternalSymbol(name) && name !== '_default') {
                        this.addSymbol(name, sourceFilePath);
                    }
                }
            }
        }
        // Handle: export function foo() {}
        else if (ts.isFunctionDeclaration(statement)) {
            const name = (statement as any).name?.text;
            if (name && !this.isAngularInternalSymbol(name)) {
                this.addSymbol(name, sourceFilePath);
            }
        }
        // Handle: export type FooType = ...
        else if (ts.isTypeAliasDeclaration(statement)) {
            const name = (statement as any).name?.text;
            if (name) {
                this.addSymbol(name, sourceFilePath);
            }
        }
        // Handle: export enum FooEnum {}
        else if (ts.isEnumDeclaration(statement)) {
            const name = (statement as any).name?.text;
            if (name) {
                this.addSymbol(name, sourceFilePath);
            }
        }
        // Handle: export { Foo, Bar }
        else if (ts.isExportDeclaration(statement)) {
            const exportClause = (statement as any).exportClause;
            if (exportClause && ts.isNamedExports(exportClause)) {
                const elements = exportClause.elements;
                for (const element of elements) {
                    const name = element.name?.text;
                    if (name && !this.isAngularInternalSymbol(name) && name !== '_default') {
                        this.addSymbol(name, sourceFilePath);
                    }
                }
            }
        }
        // Handle: export default Foo
        else if (ts.isExportAssignment(statement)) {
            if (ts.isIdentifier((statement as any).expression)) {
                const name = (statement as any).expression.text;
                if (name !== '_default' && !this.isAngularInternalSymbol(name)) {
                    this.addSymbol(name, sourceFilePath);
                }
            }
        }
    }

    /**
     * Add a symbol to the tracking map
     */
    private addSymbol(symbolName: string, declarationFile: string): void {
        if (!this.symbolToFiles.has(symbolName)) {
            this.symbolToFiles.set(symbolName, new Set<string>());
        }
        this.symbolToFiles.get(symbolName)!.add(declarationFile);
        logger.debug(`Tracked API markdown symbol: ${symbolName} from ${declarationFile}`);
    }
}

/**
 * Parse public API exports from *.api.md files in the source root
 */
export async function parseApiMarkdownExports(sourceRoot: string): Promise<ApiMarkdownExports> {
    const parser = new ApiMarkdownParser(sourceRoot);
    return await parser.parseApiMarkdownFiles();
}
