import SwiftSyntax

@SwiftSyntaxRule
struct AccessibilityLabelForImageRule: Rule, OptInRule {
    var configuration = SeverityConfiguration<Self>(.warning)

    static let description = RuleDescription(
        identifier: "accessibility_label_for_image",
        name: "Accessibility Label for Image",
        description: "Images that provide context should have an accessibility label or should be explicitly hidden " +
                     "from accessibility",
        rationale: """
        In UIKit, a `UIImageView` was by default not an accessibility element, and would only be visible to VoiceOver \
        and other assistive technologies if the developer explicitly made them an accessibility element. In SwiftUI, \
        however, an `Image` is an accessibility element by default. If the developer does not explicitly hide them \
        from accessibility or give them an accessibility label, they will inherit the name of the image file, which \
        often creates a poor experience when VoiceOver reads things like "close icon white".

        Known false negatives for Images declared as instance variables and containers that provide a label but are \
        not accessibility elements. Known false positives for Images created in a separate function from where they \
        have accessibility properties applied.
        """,
        kind: .lint,
        minSwiftVersion: .fiveDotOne,
        nonTriggeringExamples: AccessibilityLabelForImageRuleExamples.nonTriggeringExamples,
        triggeringExamples: AccessibilityLabelForImageRuleExamples.triggeringExamples
    )
}

private extension AccessibilityLabelForImageRule {
    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) {
            // Only check Image calls within View structs
            guard isInViewStruct else { return }

            // Only process direct Image calls
            guard node.isDirectImageCall else { return }

            // Use centralized exemption logic
            if !AccessibilityDeterminator.isExempt(node) {
                let violation = ReasonedRuleViolation(
                    position: node.positionAfterSkippingLeadingTrivia,
                    reason: """
                        Images that provide context should have an accessibility label or should be \
                        explicitly hidden from accessibility
                        """,
                    severity: configuration.severity
                )
                violations.append(violation)
            }
        }
    }
}

// MARK: Accessibility Exemption Logic

private struct AccessibilityDeterminator {
    /// Maximum depth to search up the syntax tree for exemptions
    static let maxSearchDepth = 20

    /// Determines if an Image call is exempt from requiring accessibility treatment
    static func isExempt(_ imageCall: FunctionCallExprSyntax) -> Bool {
        // 1. Check for decorative or labeled initializers (e.g., Image(decorative:))
        if imageCall.isDecorativeOrLabeledImage {
            return true
        }

        // 2. Check the parent hierarchy for exemptions
        return imageCall.isExemptedByAncestors()
    }
}

// MARK: SwiftSyntax extensions

private extension StructDeclSyntax {
    /// Whether this struct conforms to View protocol
    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 {
    /// Check if this is a direct Image call (not a modifier)
    var isDirectImageCall: Bool {
        // Check for direct Image call
        if let identifierExpr = calledExpression.as(DeclReferenceExprSyntax.self) {
            return identifierExpr.baseName.text == "Image"
        }

        // Check for SwiftUI.Image call
        if let memberAccessExpr = calledExpression.as(MemberAccessExprSyntax.self),
           let baseIdentifier = memberAccessExpr.base?.as(DeclReferenceExprSyntax.self) {
            return baseIdentifier.baseName.text == "SwiftUI" &&
                   memberAccessExpr.declName.baseName.text == "Image"
        }

        return false
    }

    /// Whether this is Image(decorative:) or Image(_:label:)
    var isDecorativeOrLabeledImage: Bool {
        arguments.contains { arg in
            let label = arg.label?.text
            return label == "decorative" || label == "label"
        }
    }

    /// Walks up the syntax tree to find accessibility exemptions with depth limit
    func isExemptedByAncestors() -> Bool {
        var currentNode: Syntax? = Syntax(self)
        var depth = 0

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

            // Check function calls for exempting patterns
            guard let funcCall = node.as(FunctionCallExprSyntax.self) else { continue }

            // Check for accessibility modifiers
            if let memberAccess = funcCall.calledExpression.as(MemberAccessExprSyntax.self) {
                let modifierName = memberAccess.declName.baseName.text

                if funcCall.isDirectAccessibilityModifier(modifierName) ||
                    funcCall.isContainerExemptingModifier(modifierName) {
                    return true
                }
            }

            // Check for inherently exempting containers
            if funcCall.isInherentlyExemptingContainer() {
                return true
            }

            // Check container views with accessibility modifiers
            if funcCall.isContainerView(), funcCall.hasAccessibilityModifiersInChain() {
                return true
            }

            // Stop early at statement boundaries for performance
            if node.parent?.is(StmtSyntax.self) == true {
                break
            }
        }

        return false
    }

    /// Check if this function call represents a container view
    func isContainerView() -> Bool {
        guard let identifierExpr = calledExpression.as(DeclReferenceExprSyntax.self) else { return false }
        let containerNames: Set<String> = ["VStack", "HStack", "ZStack", "Group", "LazyVStack", "LazyHStack"]
        return containerNames.contains(identifierExpr.baseName.text)
    }

    /// Check if this container has accessibility modifiers in its modifier chain
    func hasAccessibilityModifiersInChain() -> Bool {
        var currentNode: Syntax? = Syntax(self)
        var depth = 0

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

            guard let funcCall = node.as(FunctionCallExprSyntax.self),
                  let memberAccess = funcCall.calledExpression.as(MemberAccessExprSyntax.self) else { continue }

            let modifierName = memberAccess.declName.baseName.text

            if funcCall.isDirectAccessibilityModifier(modifierName) ||
                funcCall.isContainerExemptingModifier(modifierName) {
                return true
            }

            // Stop at statement boundaries
            if node.parent?.is(StmtSyntax.self) == true {
                break
            }
        }

        return false
    }

    /// Checks for modifiers like .accessibilityLabel(...) or .accessibilityHidden(true)
    func isDirectAccessibilityModifier(_ name: String) -> Bool {
        switch name {
        case "accessibilityHidden":
            return arguments.first?.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind == .keyword(.true)
        case "accessibilityLabel", "accessibilityValue", "accessibilityHint":
            return true
        case "accessibility":
            return arguments.contains { arg in
                guard let label = arg.label?.text else { return false }
                if ["label", "value", "hint"].contains(label) { return true }
                if label == "hidden" {
                    return arg.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind == .keyword(.true)
                }
                return false
            }
        default:
            return false
        }
    }

    /// Checks for modifiers that make a container exempt its children from individual accessibility
    func isContainerExemptingModifier(_ name: String) -> Bool {
        guard name == "accessibilityElement" else { return false }

        // Check for .accessibilityElement(children: .ignore) which exempts children
        if let childrenArg = arguments.first(where: { $0.label?.text == "children" }) {
            let childrenValue = childrenArg.expression.as(MemberAccessExprSyntax.self)?.declName.baseName.text
            return childrenValue == "ignore" // Only .ignore exempts individual children
        }

        // .accessibilityElement() with no arguments defaults to behavior that exempts children
        return arguments.isEmpty
    }

    /// Checks for container views that provide their own accessibility context
    func isInherentlyExemptingContainer() -> Bool {
        guard let identifier = calledExpression.as(DeclReferenceExprSyntax.self) else { return false }
        let containerName = identifier.baseName.text

        // NavigationLink automatically exempts children
        if containerName == "NavigationLink" {
            return true
        }

        // Button exempts children if it has accessibility treatment
        if containerName == "Button" {
            return hasDirectAccessibilityTreatment()
        }

        return false
    }

    /// Check if this container has direct accessibility treatment
    private func hasDirectAccessibilityTreatment() -> Bool {
        var currentNode: Syntax? = Syntax(self)
        var depth = 0

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

            guard let funcCall = node.as(FunctionCallExprSyntax.self),
                  let memberAccess = funcCall.calledExpression.as(MemberAccessExprSyntax.self) else { continue }

            let modifierName = memberAccess.declName.baseName.text
            if funcCall.isDirectAccessibilityModifier(modifierName) {
                return true
            }

            // Stop at statement boundaries
            if node.parent?.is(StmtSyntax.self) == true {
                break
            }
        }

        return false
    }
}
