import Foundation
import SwiftLintCore
import SwiftSyntax

@SwiftSyntaxRule
struct OptionalDataStringConversionRule: Rule {
    var configuration = OptionalDataStringConversionConfiguration()

    static let description = RuleDescription(
        identifier: "optional_data_string_conversion",
        name: "Optional Data -> String Conversion",
        description: "Prefer failable `String(bytes:encoding:)` initializer when converting `Data` to `String`",
        kind: .lint,
        nonTriggeringExamples: #examples([
            "String(data: data, encoding: .utf8)",
            "String(bytes: data, encoding: .utf8)",
            "String(UTF8.self)",
            "String(a, b, c, UTF8.self)",
            "String(decoding: data, encoding: UTF8.self)",
            // Additional non-triggering examples to maximize coverage:
            "String(data: data, encoding: .ascii)",
            "String(bytes: data, encoding: .utf16LittleEndian)",
            "String(decoding: data, as: UTF16.self)",
            "String.init(bytes: data, encoding: .utf8)",
            "let text: String = .init(bytes: data, encoding: .utf8)",
            "let text: String = .init(data)",
            "let text: Int = .init(decoding: data, as: UTF8.self)",
            "let n: Int = .init(0)",
            "String(repeating: \"a\", count: 3)",
            "String(format: \"%d\", 3)",
            // Default behavior (include_implicit_init == false): implicit leading-dot init without type
            "let text = .init(decoding: data, as: UTF8.self)",
        ]),
        triggeringExamples: #examples([
            "↓String(decoding: data, as: UTF8.self)",
            "↓String.init(decoding: data, as: UTF8.self)",
            "let text: String = ↓.init(decoding: data, as: UTF8.self)",
            // With include_implicit_init enabled, implicit leading-dot init also triggers
            "let text = ↓.init(decoding: data, as: UTF8.self)"
                .asExample(configuration: ["include_implicit_init": true]),
            "f(↓.init(decoding: data, as: UTF8.self))".asExample(configuration: ["include_implicit_init": true]),
        ])
    )
}

private extension OptionalDataStringConversionRule {
    final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
        override func visitPost(_ node: FunctionCallExprSyntax) {
            // Only consider calls with labels `decoding` and `as`
            guard node.arguments.map(\.label?.text) == ["decoding", "as"] else {
                return
            }
            // Check that the `as:` argument is `UTF8.self`
            guard let lastExpr = node.arguments.last?.expression.as(MemberAccessExprSyntax.self),
                  lastExpr.base?.description == "UTF8",
                  lastExpr.declName.baseName.description == "self" else {
                return
            }

            // Called expression can be:
            // 1) DeclReferenceExprSyntax("String") -> String(decoding:as:)
            // 2) MemberAccessExprSyntax(base: DeclReferenceExprSyntax("String"), declName: "init") -> String.init(...)
            // 3) MemberAccessExprSyntax(base: nil, declName: "init") -> .init(...) (leading-dot)
            let called = node.calledExpression

            // Case 1: direct `String(...)`
            if let declRef = called.as(DeclReferenceExprSyntax.self), declRef.baseName.text == "String" {
                violations.append(called.positionAfterSkippingLeadingTrivia)
                return
            }

            // Case 2 and 3: `.init` or `String.init`
            guard let member = called.as(MemberAccessExprSyntax.self), member.declName.baseName.text == "init" else {
                return
            }

            // Case 2: `String.init(...)`
            if let baseDecl = member.base?.as(DeclReferenceExprSyntax.self), baseDecl.baseName.text == "String" {
                violations.append(called.positionAfterSkippingLeadingTrivia)
                return
            }

            // Case 3: leading-dot `.init(...)`
            // This is ambiguous in general. If configuration.includeImplicitInit is true,
            // we trigger everywhere. Otherwise, we only trigger if the call is used to
            // initialize a variable that has an explicit `String` type annotation:
            // let x: String = .init(...)
            guard member.base == nil else { return }

            if configuration.includeImplicitInit || configuration.allowImplicitInit {
                violations.append(called.positionAfterSkippingLeadingTrivia)
                return
            }

            // Check if the binding has an explicit `String` type annotation
            if let binding = node.parent?.parent?.as(PatternBindingSyntax.self),
               binding.typeAnnotation?.type.description.trimmingCharacters(in: .whitespacesAndNewlines) == "String" {
                violations.append(called.positionAfterSkippingLeadingTrivia)
            }
        }
    }
}
