import SwiftSyntax

@SwiftSyntaxRule(optIn: true)
struct AccessibilityTraitForButtonRule: Rule {
    var configuration = SeverityConfiguration<Self>(.warning)

    static let description = RuleDescription(
        identifier: "accessibility_trait_for_button",
        name: "Accessibility Trait for Button",
        description: "All views with tap gestures added should include the .isButton or the .isLink accessibility " +
                     "traits",
        rationale: """
        The accessibility button and link traits are used to tell assistive technologies that an element is tappable. \
        When an element has one of these traits, VoiceOver will automatically read "button" or "link" after the \
        element's label to let the user know that they can activate it.

        When using a UIKit `UIButton` or SwiftUI `Button` or `Link`, the button trait is added by default, but when \
        you manually add a tap gesture recognizer to an element, you need to explicitly add the button or link trait. \

        In most cases the button trait should be used, but for buttons that open a URL in an external browser we use \
        the link trait instead. This rule attempts to catch uses of the SwiftUI `.onTapGesture` modifier where the \
        `.isButton` or `.isLink` trait is not explicitly applied.
        """,
        kind: .lint,
        minSwiftVersion: .fiveDotOne,
        nonTriggeringExamples: AccessibilityTraitForButtonRuleExamples.nonTriggeringExamples,
        triggeringExamples: AccessibilityTraitForButtonRuleExamples.triggeringExamples
    )
}

private extension AccessibilityTraitForButtonRule {
    final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
        private var isInViewStruct = false

        override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
            isInViewStruct = node.isViewStruct
            return .visitChildren
        }

        override func visitPost(_: StructDeclSyntax) {
            isInViewStruct = false
        }

        override func visitPost(_ node: FunctionCallExprSyntax) {
            guard isInViewStruct, node.isSingleTapGestureModifier() else {
                return
            }

            // The 'node' is the tap gesture modifier itself.
            // Check if this node, any preceding modifiers in the chain, or an encapsulating Button/Link provide
            // exemption.
            if !AccessibilityButtonTraitDeterminator.isExempt(tapGestureNode: node) {
                violations.append(
                    ReasonedRuleViolation(
                        // Position of .onTapGesture etc.
                        position: node.calledExpression.positionAfterSkippingLeadingTrivia,
                        reason: AccessibilityTraitForButtonRule.description.description,
                        severity: configuration.severity
                    )
                )
            }
        }
    }
}

private struct AccessibilityButtonTraitDeterminator {
    static let maxSearchDepth = 20 // Limit for traversing up the syntax tree

    static func isExempt(tapGestureNode: FunctionCallExprSyntax) -> Bool {
        // 1. Check if accessibility traits are present anywhere in the same modifier chain
        if hasAccessibilityTraitsInChain(tapGestureNode: tapGestureNode) {
            return true
        }

        // 2. Check if the view (to which the gesture is applied) is part of an inherently exempting container
        return isInsideInherentlyExemptingContainer(startingFrom: tapGestureNode)
    }

    private static func hasAccessibilityTraitsInChain(tapGestureNode: FunctionCallExprSyntax) -> Bool {
        // Check both directions: before the tap gesture (backwards in chain) and after (ancestors in tree)

        // 1. Check backwards in the modifier chain (modifiers applied before the tap gesture)
        if hasAccessibilityTraitsBackwards(from: tapGestureNode) {
            return true
        }

        // 2. Check forwards by looking at parent nodes (modifiers applied after the tap gesture)
        return hasAccessibilityTraitsForwards(from: tapGestureNode)
    }

    private static func hasAccessibilityTraitsBackwards(from tapGestureNode: FunctionCallExprSyntax) -> Bool {
        var current: ExprSyntax? = ExprSyntax(tapGestureNode)
        var depth = 0

        while let currentExpr = current, depth < maxSearchDepth {
            defer { depth += 1 }

            if let funcCall = currentExpr.as(FunctionCallExprSyntax.self) {
                // Check if this modifier provides accessibility traits
                if funcCall.providesButtonOrLinkTrait() || funcCall.isAccessibilityHiddenTrue() {
                    return true
                }

                // Move to the previous modifier in the chain (the base of the member access)
                if let memberAccess = funcCall.calledExpression.as(MemberAccessExprSyntax.self) {
                    current = memberAccess.base
                } else {
                    // Reached the end of the chain (e.g., Text(...))
                    break
                }
            } else {
                break
            }
        }

        return false
    }

    private static func hasAccessibilityTraitsForwards(from tapGestureNode: FunctionCallExprSyntax) -> Bool {
        // Look at parent nodes to see if accessibility traits are applied after the tap gesture
        var currentNode: Syntax? = Syntax(tapGestureNode).parent
        var depth = 0

        while let node = currentNode, depth < maxSearchDepth {
            defer {
                currentNode = node.parent
                depth += 1
            }

            // Check if this parent node is a function call with accessibility traits
            if let funcCall = node.as(FunctionCallExprSyntax.self) {
                if funcCall.providesButtonOrLinkTrait() || funcCall.isAccessibilityHiddenTrue() {
                    return true
                }
            }

            // Stop at certain boundaries
            if node.is(StmtSyntax.self) || node.is(StructDeclSyntax.self) {
                break
            }
        }

        return false
    }

    private static func isInsideInherentlyExemptingContainer(startingFrom node: FunctionCallExprSyntax) -> Bool {
        var currentNode: Syntax? = Syntax(node)
        var depth = 0

        while let currentSyntaxNode = currentNode, depth < maxSearchDepth {
            defer {
                currentNode = currentSyntaxNode.parent
                depth += 1
            }

            if let funcCall = currentSyntaxNode.as(FunctionCallExprSyntax.self),
               let identifier = funcCall.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text,
               ["Button", "Link"].contains(identifier) {
                return true
            }

            // Stop if we reach a new View declaration or similar boundary
            if currentSyntaxNode.is(StructDeclSyntax.self) ||
                currentSyntaxNode.is(ClassDeclSyntax.self) ||
                currentSyntaxNode.is(EnumDeclSyntax.self) {
                break
            }
        }
        return false
    }

    /// Helper to check if an expression (part of a gesture modifier argument) is TapGesture(count: 1) or TapGesture()
    fileprivate static func isSingleTapGestureInstance(expression: ExprSyntax) -> Bool {
        var currentExpr: ExprSyntax? = expression

        // Traverse down if it's a chain of gesture modifiers like .onEnded to find the base gesture.
        while let memberCall = currentExpr?.as(FunctionCallExprSyntax.self), // e.g. TapGesture().onEnded()
              let memberAccess = memberCall.calledExpression.as(MemberAccessExprSyntax.self),
              memberAccess.base != nil { // Ensure it's a chain like x.method()
            currentExpr = memberAccess.base
        }

        guard let gestureCall = currentExpr?.as(FunctionCallExprSyntax.self),
              let gestureName = gestureCall.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text,
              gestureName == "TapGesture" else {
            return false
        }

        // Check count argument: TapGesture() or TapGesture(count: 1)
        if gestureCall.arguments.isEmpty { return true } // Default count is 1

        if let countArg = gestureCall.arguments.first(where: { $0.label?.text == "count" }) {
            return countArg.expression.as(IntegerLiteralExprSyntax.self)?.literal.text == "1"
        }

        // If 'count' is not specified, it defaults to 1.
        return !gestureCall.arguments.contains(where: { $0.label?.text == "count" })
    }
}

private extension StructDeclSyntax {
    var isViewStruct: Bool {
        guard let inheritanceClause else { return false }
        return inheritanceClause.inheritedTypes.contains { inheritedType in
            inheritedType.type.as(IdentifierTypeSyntax.self)?.name.text == "View"
        }
    }
}

private extension FunctionCallExprSyntax {
    func isSingleTapGestureModifier() -> Bool {
        guard let calledExpr = calledExpression.as(MemberAccessExprSyntax.self) else { return false }
        let name = calledExpr.declName.baseName.text

        if name == "onTapGesture" {
            if arguments.isEmpty { return true } // Default count is 1
            if let countArg = arguments.first(where: { $0.label?.text == "count" }) {
                return countArg.expression.as(IntegerLiteralExprSyntax.self)?.literal.text == "1"
            }
            // If 'count' is not specified, it defaults to 1 (other args like 'perform' might be present)
            return !arguments.contains(where: { $0.label?.text == "count" })
        }

        if ["gesture", "simultaneousGesture", "highPriorityGesture"].contains(name) {
            guard let firstArgExpression = arguments.first?.expression else { return false }
            return AccessibilityButtonTraitDeterminator.isSingleTapGestureInstance(expression: firstArgExpression)
        }
        return false
    }

    func providesButtonOrLinkTrait() -> Bool {
        guard let calledExpr = calledExpression.as(MemberAccessExprSyntax.self) else { return false }
        let name = calledExpr.declName.baseName.text

        if name == "accessibilityAddTraits" {
            guard let firstArgExpr = arguments.first?.expression else { return false }
            return Self.expressionContainsButtonOrLinkTrait(firstArgExpr)
        }

        if name == "accessibility" {
            guard let addTraitsArg = arguments.first(where: { $0.label?.text == "addTraits" }) else { return false }
            return Self.expressionContainsButtonOrLinkTrait(addTraitsArg.expression)
        }

        return false
    }

    private static func expressionContainsButtonOrLinkTrait(_ expression: ExprSyntax) -> Bool {
        if let memberAccess = expression.as(MemberAccessExprSyntax.self) {
            let traitName = memberAccess.declName.baseName.text
            return traitName == "isButton" || traitName == "isLink"
        }
        if let arrayExpr = expression.as(ArrayExprSyntax.self) {
            return arrayExpr.elements.contains { element in
                self.expressionContainsButtonOrLinkTrait(element.expression)
            }
        }
        return false
    }

    func isAccessibilityHiddenTrue() -> Bool {
        guard let calledExpr = calledExpression.as(MemberAccessExprSyntax.self) else { return false }
        let name = calledExpr.declName.baseName.text

        if name == "accessibilityHidden" {
            return arguments.first?.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind == .keyword(.true)
        }

        if name == "accessibility" {
            guard let hiddenArg = arguments.first(where: { $0.label?.text == "hidden" }) else { return false }
            return hiddenArg.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind == .keyword(.true)
        }

        return false
    }
}
