import SwiftLintCore
import SwiftSyntax

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

    static let description = RuleDescription(
        identifier: "strict_fileprivate",
        name: "Strict Fileprivate",
        description: "`fileprivate` should be avoided",
        kind: .idiomatic,
        nonTriggeringExamples: #examples([
            "extension String {}",
            "private extension String {}",
            """
            public
                extension String {
                    var i: Int { 1 }
                }
            """,
            """
                private enum E {
                    func f() {}
                }
            """,
            """
                public struct S {
                    internal let i: Int
                }
            """,
            """
                open class C {
                    private func f() {}
                }
            """,
            """
                internal actor A {}
            """,
            """
                struct S1: P {
                    fileprivate let i = 2, j = 1
                }
                struct S2: P {
                    fileprivate var (k, l) = (1, 3)
                }
                protocol P {
                    var j: Int { get }
                    var l: Int { get }
                }
            """.asExample(excludeFromDocumentation: true),
            """
                class C: P<Int> {
                    fileprivate func f() {}
                }
                protocol P<T> {
                    func f()
                }
            """.asExample(excludeFromDocumentation: true),
        ]) + ["actor", "class", "enum", "extension", "struct"].map { type in
            """
                \(type) T: P<Int> {
                    fileprivate func f() {}
                    fileprivate let i = 3
                    public fileprivate(set) var l = 3
                }
                protocol P<T> {
                    func f()
                    var i: Int { get }
                    var l: Int { get set }
                }
            """.asExample(excludeFromDocumentation: true)
        },
        triggeringExamples: #examples([
            """
                ↓fileprivate class C {
                    ↓fileprivate func f() {}
                }
            """,
            """
                ↓fileprivate extension String {
                    ↓fileprivate var isSomething: Bool { self == "something" }
                }
            """,
            """
                ↓fileprivate actor A {
                    ↓fileprivate let i = 1
                }
            """,
            """
                ↓fileprivate struct C {
                    ↓fileprivate(set) var myInt = 4
                }
            """,
            """
                struct Outter {
                    struct Inter {
                        ↓fileprivate struct Inner {}
                    }
                }
            """,
            """
                ↓fileprivate func f() {}
            """.asExample(excludeFromDocumentation: true),
        ]) + ["actor", "class", "enum", "extension", "struct"].map { type in
            """
                \(type) T: P<Int> {
                    fileprivate func f() {}
                    ↓fileprivate func g() {}
                    fileprivate let i = 2
                    public ↓fileprivate(set) var j: Int { 1 }
                    ↓fileprivate let a = 3, b = 4
                    public ↓fileprivate(set) var k = 2
                }
                protocol P<T> {
                    func f()
                    var i: Int { get }
                    var k: Int { get }
                }
                protocol Q {
                    func g()
                    var j: Int { get }
                }
            """.asExample(excludeFromDocumentation: true)
        }
    )
}

private enum ProtocolRequirementType: Equatable {
    case method(String)
    case getter(String)
    case setter(String)
}

private extension StrictFilePrivateRule {
    final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
        private lazy var protocols = {
            ProtocolCollector(configuration: configuration, file: file)
                .walk(tree: file.syntaxTree, handler: \.protocols)
        }()

        override func visitPost(_ node: DeclModifierSyntax) {
            guard node.name.tokenKind == .keyword(.fileprivate), let grandparent = node.parent?.parent else {
                return
            }
            guard grandparent.is(FunctionDeclSyntax.self) || grandparent.is(VariableDeclSyntax.self) else {
                violations.append(node.positionAfterSkippingLeadingTrivia)
                return
            }
            let protocolMethodNames = implementedTypesInDecl(of: node).flatMap { protocols[$0, default: []] }
            if let funcDecl = grandparent.as(FunctionDeclSyntax.self),
               protocolMethodNames.contains(.method(funcDecl.name.text)) {
                return
            }
            if let varDecl = grandparent.as(VariableDeclSyntax.self) {
                let isSpecificForSetter = node.detail?.detail.tokenKind == .identifier("set")
                let firstImplementingProtocol = varDecl.bindings
                    .flatMap { binding in
                        let pattern = binding.pattern
                        if let name = pattern.as(IdentifierPatternSyntax.self)?.identifier.text {
                            return [name]
                        }
                        if let tuple = pattern.as(TuplePatternSyntax.self) {
                            return tuple.elements.compactMap {
                                $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text
                            }
                        }
                        return []
                    }
                    .first {
                        protocolMethodNames.contains(isSpecificForSetter ? .setter($0) : .getter($0))
                    }
                if firstImplementingProtocol != nil {
                    return
                }
            }
            violations.append(node.positionAfterSkippingLeadingTrivia)
        }

        private func implementedTypesInDecl(of node: (some SyntaxProtocol)?) -> [String] {
            guard let node else {
                queuedFatalError("Given node is nil. That should not happen.")
            }
            if node.is(SourceFileSyntax.self) {
                return []
            }
            if let actorDecl = node.as(ActorDeclSyntax.self) {
                return actorDecl.inheritanceClause.inheritedTypeNames
            }
            if let classDecl = node.as(ClassDeclSyntax.self) {
                return classDecl.inheritanceClause.inheritedTypeNames
            }
            if let enumDecl = node.as(EnumDeclSyntax.self) {
                return enumDecl.inheritanceClause.inheritedTypeNames
            }
            if let extensionDecl = node.as(ExtensionDeclSyntax.self) {
                return extensionDecl.inheritanceClause.inheritedTypeNames
            }
            if let structDecl = node.as(StructDeclSyntax.self) {
                return structDecl.inheritanceClause.inheritedTypeNames
            }
            return implementedTypesInDecl(of: node.parent)
        }
    }
}

private final class ProtocolCollector<Configuration: RuleConfiguration>: ViolationsSyntaxVisitor<Configuration> {
    private(set) var protocols = [String: [ProtocolRequirementType]]()
    private var currentProtocolName = ""

    override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .allExcept(ProtocolDeclSyntax.self) }

    override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
        currentProtocolName = node.name.text
        return .visitChildren
    }

    override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
        protocols[currentProtocolName, default: []].append(.method(node.name.text))
        return .skipChildren
    }

    override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
        for binding in node.bindings {
            guard let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text,
                  let accessorBlock = binding.accessorBlock else {
                continue
            }
            if accessorBlock.specifiesGetAccessor {
                protocols[currentProtocolName, default: []].append(.getter(name))
            }
            if accessorBlock.specifiesSetAccessor {
                protocols[currentProtocolName, default: []].append(.setter(name))
            }
        }
        return .skipChildren
    }
}

private extension InheritanceClauseSyntax? {
    var inheritedTypeNames: [String] {
        self?.inheritedTypes.compactMap { $0.type.as(IdentifierTypeSyntax.self)?.name.text } ?? []
    }
}
