import { createRule } from '../utils/index.js';
import { getTypeScriptTools, isAnyType } from '../utils/ts-utils/index.js';
import { findVariable } from '../utils/ast-utils.js';
import { toRegExp } from '../utils/regexp.js';
import { normalize } from 'path';
let isRemovedWarningShown = false;
export default createRule('no-unused-props', {
    meta: {
        docs: {
            description: 'Warns about defined Props properties that are unused',
            category: 'Best Practices',
            recommended: true
        },
        schema: [
            {
                type: 'object',
                properties: {
                    checkImportedTypes: {
                        type: 'boolean',
                        default: false
                    },
                    ignoreTypePatterns: {
                        type: 'array',
                        items: {
                            type: 'string'
                        },
                        default: []
                    },
                    ignorePropertyPatterns: {
                        type: 'array',
                        items: {
                            type: 'string'
                        },
                        default: []
                    },
                    allowUnusedNestedProperties: {
                        type: 'boolean',
                        default: false
                    }
                },
                additionalProperties: false
            }
        ],
        messages: {
            unusedProp: "'{{name}}' is an unused Props property.",
            unusedNestedProp: "'{{name}}' in '{{parent}}' is an unused property.",
            unusedIndexSignature: 'Index signature is unused. Consider using rest operator (...) to capture remaining properties.'
        },
        type: 'suggestion',
        conditions: [
            {
                svelteVersions: ['5'],
                runes: [true, 'undetermined']
            }
        ]
    },
    create(context) {
        const fileName = context.filename;
        const tools = getTypeScriptTools(context);
        if (!tools) {
            return {};
        }
        const typeChecker = tools.service.program.getTypeChecker();
        if (!typeChecker) {
            return {};
        }
        const options = context.options[0] ?? {};
        // TODO: Remove in v4
        // MEMO: `ignorePatterns` was a property that only existed from v3.2.0 to v3.2.2.
        // From v3.3.0, it was replaced with `ignorePropertyPatterns` and `ignoreTypePatterns`.
        if (options.ignorePatterns != null && !isRemovedWarningShown) {
            console.warn('eslint-plugin-svelte: The `ignorePatterns` option in the `no-unused-props` rule has been removed. Please use `ignorePropertyPatterns` or/and `ignoreTypePatterns` instead.');
            isRemovedWarningShown = true;
        }
        const checkImportedTypes = options.checkImportedTypes ?? false;
        const ignoreTypePatterns = (options.ignoreTypePatterns ?? []).map((p) => {
            if (typeof p === 'string') {
                return toRegExp(p);
            }
            return p;
        });
        const ignorePropertyPatterns = (options.ignorePropertyPatterns ?? []).map((p) => {
            if (typeof p === 'string') {
                return toRegExp(p);
            }
            return p;
        });
        function shouldIgnoreProperty(name) {
            return ignorePropertyPatterns.some((pattern) => pattern.test(name));
        }
        function shouldIgnoreType(type) {
            function isMatched(name) {
                return ignoreTypePatterns.some((pattern) => pattern.test(name));
            }
            const typeStr = typeChecker.typeToString(type);
            const symbol = type.getSymbol();
            const symbolName = symbol?.getName();
            return isMatched(typeStr) || (symbolName ? isMatched(symbolName) : false);
        }
        function isInternalProperty(symbol) {
            const declarations = symbol.getDeclarations();
            if (!declarations || declarations.length === 0)
                return false;
            // TypeScript declaration file name is normalized to support Windows style paths
            return declarations.every((decl) => normalize(decl.getSourceFile().fileName) === fileName);
        }
        /**
         * Extracts property paths from member expressions.
         */
        function getPropertyPath(node) {
            const paths = [];
            let isSpread = false;
            let currentNode = node;
            let parentNode = currentNode.parent ?? null;
            while (parentNode) {
                if (parentNode.type === 'MemberExpression' && parentNode.object === currentNode) {
                    const property = parentNode.property;
                    if (property.type === 'Identifier') {
                        paths.push(property.name);
                    }
                    else if (property.type === 'Literal' && typeof property.value === 'string') {
                        paths.push(property.value);
                    }
                }
                else {
                    if (parentNode.type === 'SpreadElement' || parentNode.type === 'SvelteSpreadAttribute') {
                        isSpread = true;
                    }
                    break;
                }
                currentNode = parentNode;
                parentNode = currentNode.parent ?? null;
            }
            return { paths, isSpread };
        }
        /**
         * Finds all property access paths for a given variable.
         */
        function getUsedNestedPropertyPathsArray(node) {
            const variable = findVariable(context, node);
            if (!variable)
                return { paths: [], spreadPaths: [] };
            const pathsArray = [];
            const spreadPathsArray = [];
            for (const reference of variable.references) {
                if ('identifier' in reference &&
                    reference.identifier.type === 'Identifier' &&
                    (reference.identifier.range[0] !== node.range[0] ||
                        reference.identifier.range[1] !== node.range[1])) {
                    const { paths, isSpread } = getPropertyPath(reference.identifier);
                    if (isSpread) {
                        spreadPathsArray.push(paths);
                    }
                    else {
                        pathsArray.push(paths);
                    }
                }
            }
            return { paths: pathsArray, spreadPaths: spreadPathsArray };
        }
        /**
         * Checks if a property is from TypeScript's built-in type definitions.
         * These properties should be ignored as they are not user-defined props.
         */
        function isBuiltInProperty(prop) {
            const declarations = prop.getDeclarations();
            if (!declarations || declarations.length === 0)
                return false;
            const declaration = declarations[0];
            const sourceFile = declaration.getSourceFile();
            if (!sourceFile)
                return false;
            return sourceFile.fileName.includes('node_modules/typescript/lib/');
        }
        function getUsedPropertyNamesFromPattern(pattern) {
            const usedProps = new Set();
            for (const prop of pattern.properties) {
                if (prop.type === 'Property') {
                    if (prop.key.type === 'Identifier') {
                        usedProps.add({ originalName: prop.key.name, aliasName: prop.key.name });
                    }
                    else if (prop.key.type === 'Literal' &&
                        typeof prop.key.value === 'string' &&
                        prop.value.type === 'Identifier') {
                        usedProps.add({ originalName: prop.key.value, aliasName: prop.value.name });
                    }
                }
                else if (prop.type === 'RestElement') {
                    // If there's a rest element, all properties are potentially used
                    return new Set();
                }
            }
            return usedProps;
        }
        /**
         * Check if the type is a class type (has constructor or prototype)
         */
        function isClassType(type) {
            if (!type)
                return false;
            // Check if it's a class instance type
            if (type.isClass())
                return true;
            // Check for constructor signatures
            const constructorType = type.getConstructSignatures();
            if (constructorType.length > 0)
                return true;
            // Check if it has a prototype property
            const symbol = type.getSymbol();
            if (symbol?.members?.has('prototype'))
                return true;
            return false;
        }
        /**
         * Recursively checks for unused properties in a type.
         */
        function checkUnusedProperties({ propsType, usedPropertyPaths, usedSpreadPropertyPaths, declaredPropertyNames, reportNode, parentPath, checkedPropsTypes, reportedPropertyPaths }) {
            // Skip checking if the type itself is a class
            if (isClassType(propsType))
                return;
            const typeStr = typeChecker.typeToString(propsType);
            if (checkedPropsTypes.has(typeStr))
                return;
            checkedPropsTypes.add(typeStr);
            if (shouldIgnoreType(propsType))
                return;
            const properties = typeChecker.getPropertiesOfType(propsType);
            const propsBaseTypes = propsType.getBaseTypes();
            if (!properties.length && (!propsBaseTypes || propsBaseTypes.length === 0)) {
                return;
            }
            if (propsBaseTypes) {
                for (const propsBaseType of propsBaseTypes) {
                    checkUnusedProperties({
                        propsType: propsBaseType,
                        usedPropertyPaths,
                        usedSpreadPropertyPaths,
                        declaredPropertyNames,
                        reportNode,
                        parentPath,
                        checkedPropsTypes,
                        reportedPropertyPaths
                    });
                }
            }
            for (const prop of properties) {
                if (isBuiltInProperty(prop))
                    continue;
                if (!checkImportedTypes && !isInternalProperty(prop))
                    continue;
                const propName = prop.getName();
                if (shouldIgnoreProperty(propName))
                    continue;
                const currentPath = [...parentPath, propName];
                const currentPathStr = currentPath.join('.');
                if (reportedPropertyPaths.has(currentPathStr))
                    continue;
                const propType = typeChecker.getTypeOfSymbol(prop);
                const isUsedThisInPath = usedPropertyPaths.includes(currentPathStr) ||
                    usedSpreadPropertyPaths.some((path) => {
                        return path === '' || path === currentPathStr || path.startsWith(`${currentPathStr}.`);
                    });
                const isUsedInPath = usedPropertyPaths.some((path) => {
                    return path.startsWith(`${currentPathStr}.`);
                });
                if (isUsedThisInPath && !isUsedInPath) {
                    continue;
                }
                const isUsedInProps = Array.from(declaredPropertyNames).some((p) => {
                    return p.originalName === propName;
                });
                if (!isUsedInPath && !isUsedInProps) {
                    reportedPropertyPaths.add(currentPathStr);
                    context.report({
                        node: reportNode,
                        messageId: parentPath.length ? 'unusedNestedProp' : 'unusedProp',
                        data: {
                            name: propName,
                            parent: parentPath.join('.')
                        }
                    });
                    continue;
                }
                const isUsedNested = usedPropertyPaths.some((path) => {
                    return path.startsWith(`${currentPathStr}.`);
                });
                if (isUsedNested || isUsedInProps) {
                    checkUnusedProperties({
                        propsType: propType,
                        usedPropertyPaths,
                        usedSpreadPropertyPaths,
                        declaredPropertyNames,
                        reportNode,
                        parentPath: currentPath,
                        checkedPropsTypes,
                        reportedPropertyPaths
                    });
                }
            }
            // Check for unused index signatures only at the root level
            if (parentPath.length === 0) {
                const indexType = propsType.getStringIndexType();
                const numberIndexType = propsType.getNumberIndexType();
                const hasIndexSignature = Boolean(indexType && !isAnyType(indexType, tools.ts)) ||
                    Boolean(numberIndexType && !isAnyType(numberIndexType, tools.ts));
                if (hasIndexSignature && !hasRestElement(declaredPropertyNames)) {
                    context.report({
                        node: reportNode,
                        messageId: 'unusedIndexSignature'
                    });
                }
            }
        }
        /**
         * Returns true if the destructuring pattern includes a rest element,
         * which means all remaining properties are potentially used.
         */
        function hasRestElement(declaredPropertyNames) {
            return declaredPropertyNames.size === 0;
        }
        function normalizeUsedPaths(paths, allowUnusedNestedProperties) {
            const normalized = [];
            for (const path of paths.sort((a, b) => a.length - b.length)) {
                if (normalized.some((p) => p.every((part, idx) => part === path[idx]))) {
                    continue;
                }
                normalized.push(path);
            }
            return normalized.map((path) => {
                // If we allow unused nested properties, only return first level properties
                if (allowUnusedNestedProperties)
                    return [path[0]];
                return path;
            });
        }
        return {
            'VariableDeclaration > VariableDeclarator': (node) => {
                // Only check $props declarations
                if (node.init?.type !== 'CallExpression' ||
                    node.init.callee.type !== 'Identifier' ||
                    node.init.callee.name !== '$props') {
                    return;
                }
                const tsNode = tools.service.esTreeNodeToTSNodeMap.get(node);
                if (!tsNode || !tsNode.type)
                    return;
                const propsType = typeChecker.getTypeFromTypeNode(tsNode.type);
                const usedPropertyPathsArray = [];
                const usedSpreadPropertyPathsArray = [];
                let declaredPropertyNames = new Set();
                if (node.id.type === 'ObjectPattern') {
                    declaredPropertyNames = getUsedPropertyNamesFromPattern(node.id);
                    if (declaredPropertyNames.size === 0)
                        return;
                    const identifiers = [];
                    for (const p of node.id.properties) {
                        if (p.type !== 'Property') {
                            continue;
                        }
                        if (p.value.type === 'Identifier') {
                            identifiers.push(p.value);
                        }
                        else if (p.value.type === 'AssignmentPattern' && p.value.left.type === 'Identifier') {
                            identifiers.push(p.value.left);
                        }
                    }
                    for (const identifier of identifiers) {
                        const { paths, spreadPaths } = getUsedNestedPropertyPathsArray(identifier);
                        usedPropertyPathsArray.push(...paths.map((path) => [identifier.name, ...path]));
                        usedSpreadPropertyPathsArray.push(...spreadPaths.map((path) => [identifier.name, ...path]));
                    }
                }
                else if (node.id.type === 'Identifier') {
                    const { paths, spreadPaths } = getUsedNestedPropertyPathsArray(node.id);
                    usedPropertyPathsArray.push(...paths);
                    usedSpreadPropertyPathsArray.push(...spreadPaths);
                }
                function runNormalizeUsedPaths(paths) {
                    return normalizeUsedPaths(paths, options.allowUnusedNestedProperties).map((pathArray) => {
                        return pathArray.join('.');
                    });
                }
                checkUnusedProperties({
                    propsType,
                    usedPropertyPaths: runNormalizeUsedPaths(usedPropertyPathsArray),
                    usedSpreadPropertyPaths: runNormalizeUsedPaths(usedSpreadPropertyPathsArray),
                    declaredPropertyNames,
                    reportNode: node.id,
                    parentPath: [],
                    checkedPropsTypes: new Set(),
                    reportedPropertyPaths: new Set()
                });
            }
        };
    }
});
