// ASTIndexer.Extractor.swift
// Main API for extracting symbols from Swift source code (#81)

import Foundation
import SwiftParser
import SwiftSyntax

extension ASTIndexer {
    /// Extracts symbols and imports from Swift source code using SwiftSyntax
    public struct Extractor: Sendable {
        public init() {}

        /// Extract symbols and imports from Swift source code
        /// - Parameter source: Swift source code string
        /// - Returns: Extraction result containing symbols, imports, and error status
        public func extract(from source: String) -> Result {
            // Parse source into syntax tree
            let tree = Parser.parse(source: source)

            // Create visitor and walk the tree
            let visitor = DeclarationVisitor(source: source)
            visitor.walk(tree)

            return Result(
                symbols: visitor.symbols,
                imports: visitor.imports,
                hasErrors: tree.hasError
            )
        }

        /// Extract symbols from a file URL
        /// - Parameter url: File URL to parse
        /// - Returns: Extraction result, or empty result with error if file cannot be read
        public func extract(from url: URL) -> Result {
            do {
                let source = try String(contentsOf: url, encoding: .utf8)
                return extract(from: source)
            } catch {
                return Result(
                    symbols: [],
                    imports: [],
                    hasErrors: true,
                    errorMessage: "Failed to read file: \(error.localizedDescription)"
                )
            }
        }
    }
}

// MARK: - Declaration Visitor

/// SyntaxVisitor that extracts declarations from Swift source
private final class DeclarationVisitor: SyntaxVisitor {
    private let source: String
    private let sourceLocationConverter: SourceLocationConverter

    private(set) var symbols: [ASTIndexer.Symbol] = []
    private(set) var imports: [ASTIndexer.Import] = []

    init(source: String) {
        self.source = source
        let tree = Parser.parse(source: source)
        sourceLocationConverter = SourceLocationConverter(fileName: "", tree: tree)
        super.init(viewMode: .sourceAccurate)
    }

    // MARK: - Imports

    override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind {
        let moduleName = node.path.map(\.name.text).joined(separator: ".")
        let location = node.startLocation(converter: sourceLocationConverter)
        let isExported = node.attributes.contains { attr in
            attr.as(AttributeSyntax.self)?.attributeName.trimmedDescription == "_exported"
        }

        imports.append(ASTIndexer.Import(
            moduleName: moduleName,
            line: location.line,
            isExported: isExported
        ))

        return .visitChildren
    }

    // MARK: - Type Declarations

    override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
        let symbol = extractTypeDeclaration(
            name: node.name.text,
            kind: .class,
            attributes: node.attributes,
            modifiers: node.modifiers,
            inheritanceClause: node.inheritanceClause,
            genericParameterClause: node.genericParameterClause,
            genericWhereClause: node.genericWhereClause,
            startPosition: node.positionAfterSkippingLeadingTrivia
        )
        symbols.append(symbol)
        return .visitChildren
    }

    override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
        let symbol = extractTypeDeclaration(
            name: node.name.text,
            kind: .struct,
            attributes: node.attributes,
            modifiers: node.modifiers,
            inheritanceClause: node.inheritanceClause,
            genericParameterClause: node.genericParameterClause,
            genericWhereClause: node.genericWhereClause,
            startPosition: node.positionAfterSkippingLeadingTrivia
        )
        symbols.append(symbol)
        return .visitChildren
    }

    override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
        let symbol = extractTypeDeclaration(
            name: node.name.text,
            kind: .enum,
            attributes: node.attributes,
            modifiers: node.modifiers,
            inheritanceClause: node.inheritanceClause,
            genericParameterClause: node.genericParameterClause,
            genericWhereClause: node.genericWhereClause,
            startPosition: node.positionAfterSkippingLeadingTrivia
        )
        symbols.append(symbol)
        return .visitChildren
    }

    override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
        let symbol = extractTypeDeclaration(
            name: node.name.text,
            kind: .actor,
            attributes: node.attributes,
            modifiers: node.modifiers,
            inheritanceClause: node.inheritanceClause,
            genericParameterClause: node.genericParameterClause,
            genericWhereClause: node.genericWhereClause,
            startPosition: node.positionAfterSkippingLeadingTrivia
        )
        symbols.append(symbol)
        return .visitChildren
    }

    override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
        let symbol = extractTypeDeclaration(
            name: node.name.text,
            kind: .protocol,
            attributes: node.attributes,
            modifiers: node.modifiers,
            inheritanceClause: node.inheritanceClause,
            genericParameterClause: nil,
            genericWhereClause: node.genericWhereClause,
            startPosition: node.positionAfterSkippingLeadingTrivia
        )
        symbols.append(symbol)
        return .visitChildren
    }

    override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
        let name = node.extendedType.trimmedDescription
        let location = sourceLocationConverter.location(for: node.positionAfterSkippingLeadingTrivia)
        let conformances = extractConformances(from: node.inheritanceClause)
        let attributes = extractAttributes(from: node.attributes)
        // #759 — extensions carry their constraints in the where clause:
        // `extension Collection where Element: Equatable`. Capture them so
        // the constraint axis works on extension-defined methods too.
        let genericParams = extractGenericParameters(
            from: nil,
            whereClause: node.genericWhereClause
        )

        symbols.append(ASTIndexer.Symbol(
            name: name,
            kind: .extension,
            line: location.line,
            column: location.column,
            attributes: attributes,
            conformances: conformances,
            genericParameters: genericParams
        ))

        return .visitChildren
    }

    // MARK: - Functions and Methods

    override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
        let location = sourceLocationConverter.location(for: node.positionAfterSkippingLeadingTrivia)
        let attributes = extractAttributes(from: node.attributes)
        let isPublic = hasPublicModifier(node.modifiers)
        let isStatic = hasStaticModifier(node.modifiers)
        let isAsync = node.signature.effectSpecifiers?.asyncSpecifier != nil
        let isThrows = node.signature.effectSpecifiers?.throwsClause != nil

        // Build signature
        let signature = buildFunctionSignature(node)

        // Determine if it's a method (inside a type) or free function
        let kind: ASTIndexer.SymbolKind = isInsideTypeDeclaration(node) ? .method : .function

        let genericParams = extractGenericParameters(
            from: node.genericParameterClause,
            whereClause: node.genericWhereClause
        )

        symbols.append(ASTIndexer.Symbol(
            name: node.name.text,
            kind: kind,
            line: location.line,
            column: location.column,
            signature: signature,
            isAsync: isAsync,
            isThrows: isThrows,
            isPublic: isPublic,
            isStatic: isStatic,
            attributes: attributes,
            genericParameters: genericParams
        ))

        return .visitChildren
    }

    override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
        let location = sourceLocationConverter.location(for: node.positionAfterSkippingLeadingTrivia)
        let attributes = extractAttributes(from: node.attributes)
        let isPublic = hasPublicModifier(node.modifiers)
        let isAsync = node.signature.effectSpecifiers?.asyncSpecifier != nil
        let isThrows = node.signature.effectSpecifiers?.throwsClause != nil

        let signature = "init\(node.signature.trimmedDescription)"

        symbols.append(ASTIndexer.Symbol(
            name: "init",
            kind: .initializer,
            line: location.line,
            column: location.column,
            signature: signature,
            isAsync: isAsync,
            isThrows: isThrows,
            isPublic: isPublic,
            attributes: attributes
        ))

        return .visitChildren
    }

    // MARK: - Properties

    override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
        let location = sourceLocationConverter.location(for: node.positionAfterSkippingLeadingTrivia)
        let attributes = extractAttributes(from: node.attributes)
        let isPublic = hasPublicModifier(node.modifiers)
        let isStatic = hasStaticModifier(node.modifiers)

        for binding in node.bindings {
            if let pattern = binding.pattern.as(IdentifierPatternSyntax.self) {
                symbols.append(ASTIndexer.Symbol(
                    name: pattern.identifier.text,
                    kind: .property,
                    line: location.line,
                    column: location.column,
                    isPublic: isPublic,
                    isStatic: isStatic,
                    attributes: attributes
                ))
            }
        }

        return .visitChildren
    }

    // MARK: - Enum Cases

    override func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind {
        let location = sourceLocationConverter.location(for: node.positionAfterSkippingLeadingTrivia)
        let attributes = extractAttributes(from: node.attributes)

        for element in node.elements {
            symbols.append(ASTIndexer.Symbol(
                name: element.name.text,
                kind: .case,
                line: location.line,
                column: location.column,
                attributes: attributes
            ))
        }

        return .visitChildren
    }

    // MARK: - Type Aliases

    override func visit(_ node: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind {
        let location = sourceLocationConverter.location(for: node.positionAfterSkippingLeadingTrivia)
        let attributes = extractAttributes(from: node.attributes)
        let isPublic = hasPublicModifier(node.modifiers)
        let genericParams = extractGenericParameters(
            from: node.genericParameterClause,
            whereClause: node.genericWhereClause
        )

        symbols.append(ASTIndexer.Symbol(
            name: node.name.text,
            kind: .typealias,
            line: location.line,
            column: location.column,
            isPublic: isPublic,
            attributes: attributes,
            genericParameters: genericParams
        ))

        return .visitChildren
    }

    override func visit(_ node: AssociatedTypeDeclSyntax) -> SyntaxVisitorContinueKind {
        let location = sourceLocationConverter.location(for: node.positionAfterSkippingLeadingTrivia)
        let attributes = extractAttributes(from: node.attributes)

        symbols.append(ASTIndexer.Symbol(
            name: node.name.text,
            kind: .associatedtype,
            line: location.line,
            column: location.column,
            attributes: attributes
        ))

        return .visitChildren
    }

    // MARK: - Subscripts

    override func visit(_ node: SubscriptDeclSyntax) -> SyntaxVisitorContinueKind {
        let location = sourceLocationConverter.location(for: node.positionAfterSkippingLeadingTrivia)
        let attributes = extractAttributes(from: node.attributes)
        let isPublic = hasPublicModifier(node.modifiers)
        let isStatic = hasStaticModifier(node.modifiers)

        let signature = "subscript\(node.parameterClause.trimmedDescription) -> \(node.returnClause.type.trimmedDescription)"

        symbols.append(ASTIndexer.Symbol(
            name: "subscript",
            kind: .subscript,
            line: location.line,
            column: location.column,
            signature: signature,
            isPublic: isPublic,
            isStatic: isStatic,
            attributes: attributes
        ))

        return .visitChildren
    }

    // MARK: - Macros

    override func visit(_ node: MacroDeclSyntax) -> SyntaxVisitorContinueKind {
        let location = sourceLocationConverter.location(for: node.positionAfterSkippingLeadingTrivia)
        let attributes = extractAttributes(from: node.attributes)
        let isPublic = hasPublicModifier(node.modifiers)

        symbols.append(ASTIndexer.Symbol(
            name: node.name.text,
            kind: .macro,
            line: location.line,
            column: location.column,
            isPublic: isPublic,
            attributes: attributes
        ))

        return .visitChildren
    }

    // MARK: - Helpers

    private func extractTypeDeclaration(
        name: String,
        kind: ASTIndexer.SymbolKind,
        attributes: AttributeListSyntax,
        modifiers: DeclModifierListSyntax,
        inheritanceClause: InheritanceClauseSyntax?,
        genericParameterClause: GenericParameterClauseSyntax?,
        genericWhereClause: GenericWhereClauseSyntax? = nil,
        startPosition: AbsolutePosition
    ) -> ASTIndexer.Symbol {
        let location = sourceLocationConverter.location(for: startPosition)
        let extractedAttributes = extractAttributes(from: attributes)
        let conformances = extractConformances(from: inheritanceClause)
        let isPublic = hasPublicModifier(modifiers)
        let genericParams = extractGenericParameters(
            from: genericParameterClause,
            whereClause: genericWhereClause
        )

        return ASTIndexer.Symbol(
            name: name,
            kind: kind,
            line: location.line,
            column: location.column,
            isPublic: isPublic,
            attributes: extractedAttributes,
            conformances: conformances,
            genericParameters: genericParams
        )
    }

    private func extractAttributes(from attributes: AttributeListSyntax) -> [String] {
        attributes.compactMap { element -> String? in
            guard let attr = element.as(AttributeSyntax.self) else { return nil }
            let name = attr.attributeName.trimmedDescription
            if let args = attr.arguments {
                return "@\(name)(\(args.trimmedDescription))"
            }
            return "@\(name)"
        }
    }

    private func extractConformances(from inheritanceClause: InheritanceClauseSyntax?) -> [String] {
        guard let clause = inheritanceClause else { return [] }
        return clause.inheritedTypes.map(\.type.trimmedDescription)
    }

    /// Extract entries for the `generic_params` column in `T` /
    /// `T: Constraint` form, from BOTH the generic parameter clause's
    /// inline form (`<T: Collection>`) AND the where-clause form
    /// (`where T: Collection`). Apple's DocC `declaration.code` always
    /// carries the full declaration including the where clause (see
    /// e.g. `ForEach`: `struct ForEach<Data, ID, Content> where
    /// Data : RandomAccessCollection, ID : Hashable`); the indexer's
    /// AST pass had been dropping the where-clause half until #755.
    ///
    /// Same-type requirements (`T == U`) are intentionally excluded —
    /// they're not the constraint shape `search_generics` answers.
    /// Layout requirements (rare in Apple's docs) similarly excluded.
    private func extractGenericParameters(
        from clause: GenericParameterClauseSyntax?,
        whereClause: GenericWhereClauseSyntax? = nil
    ) -> [String] {
        var entries: [String] = []

        if let clause {
            for param in clause.parameters {
                if let constraint = param.inheritedType {
                    entries.append("\(param.name.text): \(constraint.trimmedDescription)")
                } else {
                    entries.append(param.name.text)
                }
            }
        }

        if let whereClause {
            for requirement in whereClause.requirements {
                if case let .conformanceRequirement(conformance) = requirement.requirement {
                    let left = conformance.leftType.trimmedDescription
                    let right = conformance.rightType.trimmedDescription
                    entries.append("\(left): \(right)")
                }
            }
        }

        return entries
    }

    private func hasPublicModifier(_ modifiers: DeclModifierListSyntax) -> Bool {
        modifiers.contains { modifier in
            let name = modifier.name.text
            return name == "public" || name == "open"
        }
    }

    private func hasStaticModifier(_ modifiers: DeclModifierListSyntax) -> Bool {
        modifiers.contains { modifier in
            let name = modifier.name.text
            return name == "static" || name == "class"
        }
    }

    private func buildFunctionSignature(_ node: FunctionDeclSyntax) -> String {
        var sig = "func \(node.name.text)"
        if let generics = node.genericParameterClause {
            sig += generics.trimmedDescription
        }
        sig += node.signature.trimmedDescription
        return sig
    }

    private func isInsideTypeDeclaration(_ node: some SyntaxProtocol) -> Bool {
        var current: Syntax? = Syntax(node)
        while let parent = current?.parent {
            if parent.is(ClassDeclSyntax.self) ||
                parent.is(StructDeclSyntax.self) ||
                parent.is(EnumDeclSyntax.self) ||
                parent.is(ActorDeclSyntax.self) ||
                parent.is(ProtocolDeclSyntax.self) ||
                parent.is(ExtensionDeclSyntax.self) {
                return true
            }
            current = parent
        }
        return false
    }
}
