import Foundation
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

enum AutoConfigParser: MemberMacro {
    // swiftlint:disable:next function_body_length
    static func expansion(
        of _: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        conformingTo _: [TypeSyntax],
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        guard let configuration = declaration.as(StructDeclSyntax.self) else {
            context.diagnose(SwiftLintCoreMacroError.notStruct.diagnose(at: declaration))
            return []
        }
        let name = configuration.name.text
        guard name.hasSuffix("Configuration"), !name.hasSuffix("RuleConfiguration") else {
            context.diagnose(SwiftLintCoreMacroError.invalidConfigurationName.diagnose(at: configuration.name))
            return []
        }
        let ruleName = String(name.dropLast("Configuration".count)) + "Rule"
        var annotatedVarDecls = configuration.memberBlock.members
            .compactMap {
                if let varDecl = $0.decl.as(VariableDeclSyntax.self),
                   let annotation = varDecl.configurationElementAnnotation {
                    return (varDecl, annotation)
                }
                return nil
            }
        let firstIndexWithoutKey = annotatedVarDecls
            .partition { _, annotation in
                if case let .argumentList(arguments) = annotation.arguments {
                    return arguments.contains {
                           $0.label?.text == "inline"
                        && $0.expression.as(BooleanLiteralExprSyntax.self)?.literal.text == "true"
                    }
                }
                return false
            }
        let elementNames = annotatedVarDecls.compactMap {
            $0.0.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text
        }
        let nonInlinedOptions = elementNames[..<firstIndexWithoutKey]
        var inlinedOptions = elementNames[firstIndexWithoutKey...]
        let isSeverityBased = configuration.inheritanceClause?.inheritedTypes.contains {
            $0.type.as(IdentifierTypeSyntax.self)?.name.text == "SeverityBasedRuleConfiguration"
        }
        if isSeverityBased == true {
            if nonInlinedOptions.contains("severityConfiguration") {
                inlinedOptions.append("severityConfiguration")
            } else {
                context.diagnose(SwiftLintCoreMacroError.severityBasedWithoutProperty.diagnose(at: configuration.name))
            }
        }
        let applyMethod = DeclSyntax(
            try FunctionDeclSyntax("mutating func apply(configuration: Any) throws(Issue)") {
                for option in nonInlinedOptions {
                    """
                    if $\(raw: option).key.isEmpty {
                        $\(raw: option).key = "\(raw: option.snakeCased)"
                    }
                    """
                }
                for option in inlinedOptions {
                    """
                    do {
                        try \(raw: option).apply(configuration, ruleID: Parent.identifier)
                    } catch let issue where issue == Issue.nothingApplied(ruleID: Parent.identifier) {
                        // Acceptable. Continue.
                    }
                    """
                }
                """
                guard let configuration = configuration as? [String: Any] else {
                    \(raw: inlinedOptions.isEmpty
                        ? "throw .invalidConfiguration(ruleID: Parent.identifier)"
                        : "return")
                }
                """
                for option in nonInlinedOptions {
                    """
                    if let value = configuration[$\(raw: option).key] {
                        try \(raw: option).apply(value, ruleID: Parent.identifier)
                    }
                    """
                }
                """
                if !supportedKeys.isSuperset(of: configuration.keys) {
                    let unknownKeys = Set(configuration.keys).subtracting(supportedKeys)
                    Issue.invalidConfigurationKeys(ruleID: Parent.identifier, keys: unknownKeys).print()
                }
                """
                """
                try validate()
                """
            }
        )
        if configuration.genericParameterClause?.parameters.contains(where: { $0.name.text == "Parent" }) == true {
            return [applyMethod]
        }
        return [DeclSyntax("typealias Parent = \(raw: ruleName)")] + [applyMethod]
    }
}

enum AcceptableByConfigurationElement: ExtensionMacro {
    static func expansion(
        of _: AttributeSyntax,
        attachedTo declaration: some DeclGroupSyntax,
        providingExtensionsOf type: some TypeSyntaxProtocol,
        conformingTo _: [TypeSyntax],
        in context: some MacroExpansionContext
    ) throws -> [ExtensionDeclSyntax] {
        guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
            context.diagnose(SwiftLintCoreMacroError.notEnum.diagnose(at: declaration))
            return []
        }
        guard enumDecl.hasStringRawType else {
            context.diagnose(SwiftLintCoreMacroError.noStringRawType.diagnose(at: declaration))
            return []
        }
        let accessLevel = enumDecl.accessLevel
        return [
            try ExtensionDeclSyntax("""
                extension \(type): AcceptableByConfigurationElement {
                    \(raw: accessLevel)func asOption() -> OptionType { .symbol(rawValue) }
                    \(raw: accessLevel)init(fromAny value: Any, context ruleID: String) throws(Issue) {
                        if let value = value as? String, let newSelf = Self(rawValue: value) {
                            self = newSelf
                        } else {
                            throw .invalidConfiguration(ruleID: ruleID)
                        }
                    }
                }
                """),
        ]
    }
}

private extension VariableDeclSyntax {
    var configurationElementAnnotation: AttributeSyntax? {
        let attribute = attributes.first {
            if let attr = $0.as(AttributeSyntax.self), let attrId = attr.attributeName.as(IdentifierTypeSyntax.self) {
                return attrId.name.text == "ConfigurationElement"
            }
            return false
        }
        return if case let .attribute(unwrapped) = attribute { unwrapped } else { nil }
    }
}

private extension EnumDeclSyntax {
    var hasStringRawType: Bool {
        if let inheritanceClause {
            return inheritanceClause.inheritedTypes.contains {
                $0.type.as(IdentifierTypeSyntax.self)?.name.text == "String"
            }
        }
        return false
    }

    var accessLevel: String {
        modifiers.compactMap {
            switch $0.name.tokenKind {
            case .keyword(.public): "public "
            case .keyword(.package): "package "
            case .keyword(.private): "private "
            default: nil
            }
        }.first ?? ""
    }
}

private extension String {
    // swiftlint:disable:next force_try
    static let regex = try! NSRegularExpression(pattern: "(?<!^)(?=[A-Z])")

    var snakeCased: Self {
        Self.regex.stringByReplacingMatches(
            in: self,
            range: NSRange(location: 0, length: utf16.count),
            withTemplate: "_"
        ).lowercased()
    }
}
