import Foundation

// swiftlint:disable file_length

/// A type that can be converted into a human-readable representation.
public protocol Documentable {
    /// Indicate if the item has some content that is useful to document.
    var hasContent: Bool { get }

    /// Convert an object to Markdown.
    ///
    /// - Returns: A Markdown string describing the object.
    func markdown() -> String

    /// Convert an object to a single line string.
    ///
    /// - Returns: A "one liner" describing the object.
    func oneLiner() -> String

    /// Convert an object to YAML as used in `.swiftlint.yml`.
    ///
    /// - Returns: A YAML snippet that can be used in configuration files.
    func yaml() -> String
}

/// Description of a rule configuration.
public struct RuleConfigurationDescription: Equatable, Sendable {
    fileprivate let options: [RuleConfigurationOption]

    fileprivate init(options: [RuleConfigurationOption], exclusiveOptions: Set<String> = []) {
        if options.contains(.noOptions) {
            if options.count > 1 {
                queuedFatalError(
                    """
                    Cannot create a configuration description with a mixture of `noOption`
                    and other options or multiple `noOptions`s. If any, descriptions must only
                    contain one single no-documentation marker.
                    """
                )
            }
            self.options = []
            return
        }
        let nonEmptyOptions = options.filter { $0.value != .empty }
        self.options = exclusiveOptions.isEmpty
            ? nonEmptyOptions
            : nonEmptyOptions.filter { exclusiveOptions.contains($0.key) }
    }

    static func from(configuration: some RuleConfiguration, exclusiveOptions: Set<String> = []) -> Self {
        // Prefer custom descriptions.
        if let customDescription = configuration.parameterDescription {
            return Self(options: customDescription.options, exclusiveOptions: exclusiveOptions)
        }
        let options: [RuleConfigurationOption] = Mirror(reflecting: configuration).children
            .flatMap { child in
                // Property wrappers have names prefixed by an underscore.
                if child.label?.starts(with: "_") == true,
                   let element = child.value as? any AnyConfigurationElement {
                    return element.description.options
                }
                return []
            }
        guard options.isNotEmpty else {
            queuedFatalError(
                """
                Rule configuration '\(configuration)' does not have any parameters.
                A custom description must be provided. If really no documentation is
                required, define the description as `{ RuleConfigurationOption.noOptions }`.
                """
            )
        }
        return Self(options: options, exclusiveOptions: exclusiveOptions)
    }

    func allowedKeys() -> [String] {
        options.flatMap { option -> [String] in
            switch option.value {
            case let .nested(nestedConfiguration) where option.key.isEmpty:
                nestedConfiguration.allowedKeys()
            case .empty:
                []
            default:
                [option.key]
            }
        }
    }
}

extension RuleConfigurationDescription: Documentable {
    public var hasContent: Bool {
        options.isNotEmpty
    }

    public func oneLiner() -> String {
        oneLiner(separator: ";")
    }

    fileprivate func oneLiner(separator: String) -> String {
        options.map { $0.oneLiner() }.joined(separator: "\(separator) ")
    }

    public func markdown() -> String {
        guard hasContent else {
            return ""
        }
        let includesDocumentationColumn = options.contains(where: \.hasDocumentation)
        let header = includesDocumentationColumn
            ? "<tr><th>Key</th><th>Value</th><th>Description</th></tr>"
            : "<tr><th>Key</th><th>Value</th></tr>"
        return """
            <table>
            <thead>
            \(header)
            </thead>
            <tbody>
            \(options
                .map { $0.markdown(includeDocumentationColumn: includesDocumentationColumn) }
                .joined(separator: "\n"))
            </tbody>
            </table>
            """
    }

    public func yaml() -> String {
        options.map { $0.yaml() }.joined(separator: "\n")
    }
}

/// A single option of a ``RuleConfigurationDescription``.
public struct RuleConfigurationOption: Equatable, Sendable {
    /// An option serving as a marker for an empty configuration description.
    public static let noOptions = Self(key: "<nothing>", value: .empty, documentation: nil)

    fileprivate let key: String
    fileprivate let value: OptionType
    fileprivate let documentation: String?

    fileprivate var hasDocumentation: Bool {
        documentation?.trimmingCharacters(in: .whitespacesAndNewlines).isNotEmpty == true
    }
}

extension RuleConfigurationOption: Documentable {
    public var hasContent: Bool {
        self != .noOptions
    }

    public func markdown() -> String {
        markdown(includeDocumentationColumn: hasDocumentation)
    }

    fileprivate func markdown(includeDocumentationColumn: Bool) -> String {
        if includeDocumentationColumn {
            return """
                <tr>
                <td>
                \(key)
                </td>
                <td>
                \(value.markdown())
                </td>
                <td>
                \(documentation ?? "-")
                </td>
                </tr>
                """
        }
        return """
            <tr>
            <td>
            \(key)
            </td>
            <td>
            \(value.markdown())
            </td>
            </tr>
            """
    }

    public func oneLiner() -> String {
        "\(key): \(value.oneLiner())"
    }

    public func yaml() -> String {
        if case .nested = value {
            return """
                \(key):
                \(value.yaml().indent(by: 2))
                """
        }
        return "\(key): \(value.yaml())"
    }
}

/// Type of an option.
public enum OptionType: Equatable, Sendable {
    /// An irrelevant option. It will be ignored in documentation serialization.
    case empty
    /// A boolean flag.
    case flag(Bool)
    /// A string option.
    case string(String)
    /// Like a string option but without quotes in the serialized output.
    case symbol(String)
    /// An integer option.
    case integer(Int)
    /// A floating point number option.
    case float(Double)
    /// Special option for a ``ViolationSeverity``.
    case severity(ViolationSeverity)
    /// A list of options.
    case list([Self])
    /// An option which is another set of configuration options to be nested in the serialized output.
    case nested(RuleConfigurationDescription)
}

extension OptionType: Documentable {
    public var hasContent: Bool {
        self != .empty
    }

    public func markdown() -> String {
        switch self {
        case .empty, .flag, .symbol, .integer, .float, .severity:
            return yaml()
        case let .string(value):
            return "&quot;" + value + "&quot;"
        case let .list(options):
            return "[" + options.map { $0.markdown() }.joined(separator: ", ") + "]"
        case let .nested(value):
            return value.markdown()
        }
    }

    public func oneLiner() -> String {
        if case let .nested(value) = self {
            return value.oneLiner(separator: ",")
        }
        return yaml()
    }

    public func yaml() -> String {
        switch self {
        case .empty:
            queuedFatalError("Empty options shall not be serialized.")
        case let .flag(value):
            return String(describing: value)
        case let .string(value):
            return "\"" + value + "\""
        case let .symbol(value):
            return value
        case let .integer(value):
            return String(describing: value)
        case let .float(value):
            return String(describing: value)
        case let .severity(value):
            return value.rawValue
        case let .list(options):
            return "[" + options.map { $0.oneLiner() }.joined(separator: ", ") + "]"
        case let .nested(value):
            return value.yaml()
        }
    }
}

// MARK: Result builder

/// A result builder creating configuration descriptions.
@resultBuilder
public struct RuleConfigurationDescriptionBuilder {
    /// :nodoc:
    public typealias Description = RuleConfigurationDescription

    /// :nodoc:
    public static func buildBlock(_ components: Description...) -> Description {
        Self.buildArray(components)
    }

    /// :nodoc:
    public static func buildOptional(_ component: Description?) -> Description {
        component ?? Description(options: [])
    }

    /// :nodoc:
    public static func buildEither(first component: Description) -> Description {
        component
    }

    /// :nodoc:
    public static func buildEither(second component: Description) -> Description {
        component
    }

    /// :nodoc:
    public static func buildExpression(_ expression: RuleConfigurationOption) -> Description {
        Description(options: [expression])
    }

    /// :nodoc:
    public static func buildExpression(_ expression: some RuleConfiguration) -> Description {
        Description.from(configuration: expression)
    }

    /// :nodoc:
    public static func buildArray(_ components: [Description]) -> Description {
        Description(options: components.flatMap(\.options))
    }
}

infix operator =>: MultiplicationPrecedence

public extension OptionType {
    /// Operator enabling an easy way to create a configuration option.
    ///
    /// - Parameters:
    ///   - key: Name of the option.
    ///   - value: Value of the option.
    ///
    /// - Returns: A configuration option built up by the given data.
    static func => (key: String, value: OptionType) -> RuleConfigurationOption {
        RuleConfigurationOption(key: key, value: value, documentation: nil)
    }

    /// Create an option defined by nested configuration description.
    ///
    /// - Parameters:
    ///   - description: A configuration description buildable by applying the result builder syntax.
    ///
    /// - Returns: A configuration option with a value being another configuration description.
    static func nest(@RuleConfigurationDescriptionBuilder _ description: () -> RuleConfigurationDescription) -> Self {
        .nested(description())
    }
}

// MARK: Property wrapper

/// Type of a configuration parameter wrapper.
private protocol AnyConfigurationElement {
    var description: RuleConfigurationDescription { get }
}

/// Type of an object that can be used as a configuration element.
public protocol AcceptableByConfigurationElement {
    /// Initializer taking a value from a configuration to create an element of `Self`.
    ///
    /// - Parameters:
    ///   - value: Value from a configuration.
    ///   - ruleID: The rule's identifier in which context the configuration parsing runs.
    init(fromAny value: Any, context ruleID: String) throws(Issue)

    /// Make the object an option.
    ///
    /// - Returns: Option representing the object.
    func asOption() -> OptionType

    /// Make the object a description.
    ///
    /// - Parameters:
    ///   - key: Name of the option to be put into the description.
    ///
    /// - Returns: Configuration description of this object.
    func asDescription(with key: String, documentation: String?) -> RuleConfigurationDescription

    /// Update the object.
    ///
    /// - Parameters:
    ///   - value: New underlying data for the object.
    ///   - ruleID: The rule's identifier in which context the configuration parsing runs.
    mutating func apply(_ value: Any, ruleID: String) throws(Issue)
}

/// Default implementations which are shortcuts applicable for most of the types conforming to the protocol.
public extension AcceptableByConfigurationElement {
    func asDescription(with key: String, documentation: String? = nil) -> RuleConfigurationDescription {
        asDescriptionImpl(key: key, option: asOption(), documentation: documentation)
    }

    fileprivate func asDescriptionImpl(key: String,
                                       option: OptionType? = nil,
                                       documentation: String? = nil) -> RuleConfigurationDescription {
        let option = option ?? asOption()
        if let documentation, documentation.trimmingCharacters(in: .whitespacesAndNewlines).isNotEmpty {
            return RuleConfigurationDescription(options: [
                RuleConfigurationOption(
                    key: key,
                    value: option,
                    documentation: documentation
                ),
            ])
        }
        return RuleConfigurationDescription(options: [key => option])
    }

    mutating func apply(_ value: Any, ruleID: String) throws(Issue) {
        self = try Self(fromAny: value, context: ruleID)
    }
}

/// An option type that can appear inlined into its using configuration.
///
/// The ``ConfigurationElement`` must opt into this behavior. In this case, the option does not have a key. This is
/// almost exclusively useful for common ``RuleConfiguration``s that are used in many other rules as child
/// configurations.
///
/// > Warning: A type conforming to this protocol is assumed to throw an issue in its `apply` method only when it's
/// absolutely clear that there is an error in the YAML configuration passed in. Since it may be used in a nested
/// context and doesn't know about the outer configuration, it's not always clear if a certain key-value is really
/// unacceptable.
public protocol InlinableOptionType: AcceptableByConfigurationElement {}

/// A single parameter of a rule configuration.
///
/// Apply it to a simple (e.g. boolean) property like
/// ```swift
/// @ConfigurationElement
/// var property = true
/// ```
/// to add a (boolean) option to a configuration. The name of the option will be inferred from the name of the property.
/// In this case, it's just `property`. CamelCase names will translated into snake_case, i.e. `myOption` is going to be
/// translated into `my_option` in the `.swiftlint.yml` configuration file.
///
/// This mechanism may be overwritten with an explicitly set key:
/// ```swift
/// @ConfigurationElement(key: "foo_bar")
/// var property = true
/// ```
///
/// If the wrapped element is an ``InlinableOptionType``, there are three ways to represent it in the documentation:
///
/// 1. It can be inlined into the parent configuration. For that, add the parameter `inline: true`. E.g.
///    ```swift
///    @ConfigurationElement(inline: true)
///    var levels = SeverityLevelsConfiguration(warning: 1, error: 2)
///    ```
///    will be documented as a linear list:
///    ```
///    warning: 1
///    error: 2
///    ```
/// 2. It can be represented as a separate nested configuration. In this case, it must not have set the `inline` flag to
/// `true`. E.g.
///    ```swift
///    @ConfigurationElement
///    var levels = SeverityLevelsConfiguration(warning: 1, error: 2)
///    ```
///    will have a nested configuration section:
///    ```
///    levels: warning: 1
///            error: 2
///    ```
/// 3. As mentioned in the beginning, the implicit key inference mechanism can be overruled by specifying a `key` as in:
///    ```swift
///    @ConfigurationElement(key: "foo")
///    var levels = SeverityLevelsConfiguration(warning: 1, error: 2)
///    ```
///    It will appear in the documentation as:
///    ```
///    foo: warning: 1
///         error: 2
///    ```
///
@propertyWrapper
public struct ConfigurationElement<T: AcceptableByConfigurationElement & Equatable & Sendable>: Equatable, Sendable {
    /// A deprecation notice.
    public enum DeprecationNotice: Sendable {
        /// Warning suggesting an alternative option.
        case suggestAlternative(ruleID: String, name: String)
    }

    /// Wrapped option value.
    public var wrappedValue: T {
        didSet {
            if case let .suggestAlternative(id, name) = deprecationNotice {
                Issue.deprecatedConfigurationOption(ruleID: id, key: key, alternative: name).print()
            }
            if wrappedValue != oldValue {
                rawWrappedValue = wrappedValue
                postprocessor(&wrappedValue)
            }
        }
    }

    /// The original value without postprocessing applied. It will be used for documentation to not mislead users with
    /// values that are modified by the postprocessor for internal reasons.
    private var rawWrappedValue: T

    /// The wrapper itself providing access to all its data. This field can only be accessed by the
    /// element's name prefixed with a `$`.
    public var projectedValue: Self {
        get { self }
        _modify { yield &self }
    }

    /// Name of this configuration entry.
    public var key: String

    /// Whether this configuration element will be inlined into its description.
    public let inline: Bool

    /// Optional documentation describing this configuration entry in rendered docs.
    public let documentation: String?

    private let deprecationNotice: DeprecationNotice?
    private let documentPostprocessedValue: Bool
    private let postprocessor: @Sendable (inout T) -> Void

    /// Default constructor.
    ///
    /// - Parameters:
    ///   - value: Value to be wrapped.
    ///   - key: Optional name of the option. If not specified, it will be inferred from the attributed property.
    ///   - deprecationNotice: An optional deprecation notice in case an option is outdated and/or has been replaced by
    ///                        an alternative.
    ///   - documentPostprocessedValue: Whether the value after postprocessing should be used for documentation. By
    ///                                 default, the value after postprocessing is used, but in some cases, it may be
    ///                                 desirable to document the original value without modifications applied by the
    ///                                 postprocessor. In this case, set this parameter to `false`. Note that the
    ///                                 postprocessor will still be applied to the wrapped value and the original value
    ///                                 will only be used for documentation.
    ///   - postprocessor: Function to be applied to the wrapped value after parsing to validate and modify it.
    @preconcurrency
    public init(wrappedValue value: T,
                key: String,
                documentation: String? = nil,
                deprecationNotice: DeprecationNotice? = nil,
                documentPostprocessedValue: Bool = true,
                postprocessor: @escaping @Sendable (inout T) -> Void = { _ in }) {
        // swiftlint:disable:previous no_empty_block
        self.init(
            wrappedValue: value,
            key: key,
            inline: false,
            documentation: documentation,
            deprecationNotice: deprecationNotice,
            documentPostprocessedValue: documentPostprocessedValue,
            postprocessor: postprocessor
        )

        // Modify the set value immediately.
        postprocessor(&wrappedValue)
    }

    /// Constructor for optional values.
    ///
    /// It allows to skip explicit initialization of the property with `nil`.
    ///
    /// - Parameters:
    ///   - key: Optional name of the option. If not specified, it will be inferred from the attributed property.
    ///   - documentation: Optional documentation describing the option in rendered docs.
    public init<Wrapped>(key: String, documentation: String? = nil) where T == Wrapped? {
        self.init(wrappedValue: nil, key: key, inline: false, documentation: documentation)
    }

    /// Constructor for an ``InlinableOptionType`` without a key.
    ///
    /// - Parameters:
    ///   - value: Value to be wrapped.
    ///   - inline: If `true`, the option will be handled as it would be part of its parent. All of its options
    ///             will be inlined. Otherwise, it will be treated as a normal nested configuration with its name
    ///             inferred from the name of the attributed property.
    ///   - documentation: Optional documentation describing the option in rendered docs.
    public init(wrappedValue value: T, inline: Bool, documentation: String? = nil) where T: InlinableOptionType {
        assert(inline, "Only 'inline: true' is allowed at the moment.")
        self.init(wrappedValue: value, key: "", inline: inline, documentation: documentation)
    }

    /// Constructor for an ``InlinableOptionType`` with a name. The configuration will explicitly not be inlined.
    ///
    /// - Parameters:
    ///   - value: Value to be wrapped.
    ///   - key: Name of the option.
    ///   - documentation: Optional documentation describing the option in rendered docs.
    public init(wrappedValue value: T, key: String, documentation: String? = nil) where T: InlinableOptionType {
        self.init(wrappedValue: value, key: key, inline: false, documentation: documentation)
    }

    private init(wrappedValue: T,
                 key: String,
                 inline: Bool,
                 documentation: String? = nil,
                 deprecationNotice: DeprecationNotice? = nil,
                 documentPostprocessedValue: Bool = true,
                 postprocessor: @escaping @Sendable (inout T) -> Void = { _ in }) {
        // swiftlint:disable:previous no_empty_block
        self.rawWrappedValue = wrappedValue
        self.wrappedValue = wrappedValue
        self.key = key
        self.inline = inline
        self.documentation = documentation
        self.deprecationNotice = deprecationNotice
        self.documentPostprocessedValue = documentPostprocessedValue
        self.postprocessor = postprocessor
    }

    public static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.wrappedValue == rhs.wrappedValue && lhs.key == rhs.key
    }
}

extension ConfigurationElement: AnyConfigurationElement {
    fileprivate var description: RuleConfigurationDescription {
        (documentPostprocessedValue ? wrappedValue : rawWrappedValue).asDescription(
            with: key,
            documentation: documentation
        )
    }
}

// MARK: AcceptableByConfigurationElement conformances

extension Optional: AcceptableByConfigurationElement where Wrapped: AcceptableByConfigurationElement {
    public func asOption() -> OptionType {
        self?.asOption() ?? .empty
    }

    public init(fromAny value: Any, context ruleID: String) throws(Issue) {
        self = try Wrapped(fromAny: value, context: ruleID)
    }
}

struct Symbol: Equatable, AcceptableByConfigurationElement {
    let value: String

    func asOption() -> OptionType {
        .symbol(value)
    }

    init(fromAny value: Any, context ruleID: String) throws(Issue) {
        guard let value = value as? String else {
            throw .invalidConfiguration(ruleID: ruleID)
        }
        self.value = value
    }
}

extension Bool: AcceptableByConfigurationElement {
    public func asOption() -> OptionType {
        .flag(self)
    }

    public init(fromAny value: Any, context ruleID: String) throws(Issue) {
        guard let value = value as? Self else {
            throw .invalidConfiguration(ruleID: ruleID)
        }
        self = value
    }
}

extension String: AcceptableByConfigurationElement {
    public func asOption() -> OptionType {
        .string(self)
    }

    public init(fromAny value: Any, context ruleID: String) throws(Issue) {
        guard let value = value as? Self else {
            throw .invalidConfiguration(ruleID: ruleID)
        }
        self = value
    }
}

extension Array: AcceptableByConfigurationElement where Element: AcceptableByConfigurationElement {
    public func asOption() -> OptionType {
        .list(map { $0.asOption() })
    }

    public init(fromAny value: Any, context ruleID: String) throws(Issue) {
        let values = value as? [Any] ?? [value]
        self = try values.map { value throws(Issue) in
            try Element(fromAny: value, context: ruleID)
        }
    }
}

extension Set: AcceptableByConfigurationElement where Element: AcceptableByConfigurationElement & Comparable {
    public func asOption() -> OptionType {
        sorted().asOption()
    }

    public init(fromAny value: Any, context ruleID: String) throws(Issue) {
        self = Set(try [Element].init(fromAny: value, context: ruleID))
    }
}

extension Int: AcceptableByConfigurationElement {
    public func asOption() -> OptionType {
        .integer(self)
    }

    public init(fromAny value: Any, context ruleID: String) throws(Issue) {
        guard let value = value as? Self else {
            throw .invalidConfiguration(ruleID: ruleID)
        }
        self = value
    }
}

extension Double: AcceptableByConfigurationElement {
    public func asOption() -> OptionType {
        .float(self)
    }

    public init(fromAny value: Any, context ruleID: String) throws(Issue) {
        if let value = value as? Self {
            self = value
        } else if let value = value as? Int {
            self = Double(value)
        } else {
            throw .invalidConfiguration(ruleID: ruleID)
        }
    }
}

extension RegularExpression: AcceptableByConfigurationElement {
    public func asOption() -> OptionType {
        .string(pattern)
    }

    public init(fromAny value: Any, context ruleID: String) throws(Issue) {
        guard let value = value as? String else {
            throw .invalidConfiguration(ruleID: ruleID)
        }
        self = try .from(pattern: value, for: ruleID)
    }
}

// MARK: RuleConfiguration conformances

public extension AcceptableByConfigurationElement where Self: RuleConfiguration {
    func asOption() -> OptionType {
        .nested(.from(configuration: self))
    }

    func asDescription(with key: String, documentation: String? = nil) -> RuleConfigurationDescription {
        if key.isEmpty {
            return .from(configuration: self)
        }
        return asDescriptionImpl(key: key, documentation: documentation)
    }

    mutating func apply(_ value: Any, ruleID _: String) throws(Issue) {
        try apply(configuration: value)
    }

    init(fromAny _: Any, context _: String) throws(Issue) {
        throw .genericError("Do not call this initializer")
    }
}

public extension SeverityConfiguration {
    /// Severity configurations are special in that they shall not be nested when an option name is provided.
    /// Instead, their only option value must be used together with the option name.
    func asDescription(with key: String, documentation: String? = nil) -> RuleConfigurationDescription {
        let description = RuleConfigurationDescription.from(configuration: self)
        if key.isEmpty {
            return description
        }
        guard let option = description.options.onlyElement?.value, case .symbol = option else {
            queuedFatalError(
                """
                Severity configurations must have exactly one option that is a violation severity.
                """
            )
        }
        return asDescriptionImpl(key: key, option: option, documentation: documentation)
    }
}
