#if os(macOS)
@preconcurrency import Darwin
#endif
import Dispatch
import Foundation
import SourceKittenFramework

// swiftlint:disable file_length

package enum LintOrAnalyzeMode {
    case lint, analyze

    package var imperative: String {
        switch self {
        case .lint:
            return "lint"
        case .analyze:
            return "analyze"
        }
    }

    package var verb: String {
        switch self {
        case .lint:
            return "linting"
        case .analyze:
            return "analyzing"
        }
    }
}

package struct LintOrAnalyzeOptions {
    let mode: LintOrAnalyzeMode
    let paths: [URL]
    let useSTDIN: Bool
    let configurationFiles: [URL]
    let strict: Bool
    let lenient: Bool
    let forceExclude: Bool
    let useExcludingByPrefix: Bool
    let useScriptInputFiles: Bool
    let useScriptInputFileLists: Bool
    let benchmark: Bool
    let reporter: String?
    let baseline: URL?
    let writeBaseline: URL?
    let workingDirectory: String?
    let quiet: Bool
    let output: URL?
    let progress: Bool
    let cachePath: String?
    let ignoreCache: Bool
    let enableAllRules: Bool
    let onlyRule: [String]
    let autocorrect: Bool
    let format: Bool
    let disableSourceKit: Bool
    let compilerLogPath: String?
    let compileCommands: String?
    let checkForUpdates: Bool

    package init(mode: LintOrAnalyzeMode,
                 paths: [URL],
                 useSTDIN: Bool,
                 configurationFiles: [URL],
                 strict: Bool,
                 lenient: Bool,
                 forceExclude: Bool,
                 useExcludingByPrefix: Bool,
                 useScriptInputFiles: Bool,
                 useScriptInputFileLists: Bool,
                 benchmark: Bool,
                 reporter: String?,
                 baseline: URL?,
                 writeBaseline: URL?,
                 workingDirectory: String?,
                 quiet: Bool,
                 output: URL?,
                 progress: Bool,
                 cachePath: String?,
                 ignoreCache: Bool,
                 enableAllRules: Bool,
                 onlyRule: [String],
                 autocorrect: Bool,
                 format: Bool,
                 disableSourceKit: Bool,
                 compilerLogPath: String?,
                 compileCommands: String?,
                 checkForUpdates: Bool) {
        self.mode = mode
        self.paths = paths
        self.useSTDIN = useSTDIN
        self.configurationFiles = configurationFiles
        self.strict = strict
        self.lenient = lenient
        self.forceExclude = forceExclude
        self.useExcludingByPrefix = useExcludingByPrefix
        self.useScriptInputFiles = useScriptInputFiles
        self.useScriptInputFileLists = useScriptInputFileLists
        self.benchmark = benchmark
        self.reporter = reporter
        self.baseline = baseline
        self.writeBaseline = writeBaseline
        self.workingDirectory = workingDirectory
        self.quiet = quiet
        self.output = output
        self.progress = progress
        self.cachePath = cachePath
        self.ignoreCache = ignoreCache
        self.enableAllRules = enableAllRules
        self.onlyRule = onlyRule
        self.autocorrect = autocorrect
        self.format = format
        self.disableSourceKit = disableSourceKit
        self.compilerLogPath = compilerLogPath
        self.compileCommands = compileCommands
        self.checkForUpdates = checkForUpdates
    }

    var verb: String {
        autocorrect ? "correcting" : mode.verb
    }

    var capitalizedVerb: String {
        verb.capitalized
    }
}

package struct LintOrAnalyzeCommand {
    package static func run(_ options: LintOrAnalyzeOptions) async throws {
        Request.disableSourceKitOverride = options.mode == .lint && options.disableSourceKit
        if let workingDirectory = options.workingDirectory {
            let currentDirectory = FileManager.default.currentDirectoryPath
            defer {
                if !FileManager.default.changeCurrentDirectoryPath(currentDirectory) {
                    queuedFatalError("Could not change back to the original directory '\(currentDirectory)'.")
                }
            }
            if !FileManager.default.changeCurrentDirectoryPath(workingDirectory) {
                throw SwiftLintError.usageError(
                    description: """
                                 Could not change working directory to '\(workingDirectory)'. \
                                 Make sure it exists and is accessible.
                                 """
                )
            }
        }
        try await Signposts.record(name: "LintOrAnalyzeCommand.run") {
            try await options.autocorrect ? autocorrect(options) : lintOrAnalyze(options)
        }
    }

    private static func lintOrAnalyze(_ options: LintOrAnalyzeOptions) async throws {
        let builder = LintOrAnalyzeResultBuilder(options)
        let files = try await collectViolations(builder: builder)
        if let baselineOutputPath = options.writeBaseline ?? builder.configuration.writeBaseline {
            try Baseline(violations: builder.unfilteredViolations).write(toPath: baselineOutputPath)
        }
        let numberOfSeriousViolations = try Signposts.record(name: "LintOrAnalyzeCommand.PostProcessViolations") {
            try postProcessViolations(files: files, builder: builder)
        }
        if options.checkForUpdates || builder.configuration.checkForUpdates {
            await UpdateChecker.checkForUpdates()
        }
        if numberOfSeriousViolations > 0 {
            exit(2)
        }
    }

    private static func collectViolations(builder: LintOrAnalyzeResultBuilder) async throws -> [SwiftLintFile] {
        let options = builder.options
        let visitorMutationQueue = DispatchQueue(label: "io.realm.swiftlint.lintVisitorMutation")
        let baseline = try baseline(options, builder.configuration)
        return try await builder.configuration.visitLintableFiles(options: options, cache: builder.cache,
                                                                  storage: builder.storage) { linter in
            let currentViolations: [StyleViolation]
            if options.benchmark {
                CustomRuleTimer.shared.activate()
                let start = Date()
                let (violationsBeforeLeniency, currentRuleTimes) = linter
                    .styleViolationsAndRuleTimes(using: builder.storage)
                currentViolations = applyLeniency(
                    options: options,
                    strict: builder.configuration.strict,
                    lenient: builder.configuration.lenient,
                    violations: violationsBeforeLeniency
                )
                visitorMutationQueue.sync {
                    builder.fileBenchmark.record(file: linter.file, from: start)
                    currentRuleTimes.forEach { builder.ruleBenchmark.record(id: $0, time: $1) }
                }
            } else {
                currentViolations = applyLeniency(
                    options: options,
                    strict: builder.configuration.strict,
                    lenient: builder.configuration.lenient,
                    violations: linter.styleViolations(using: builder.storage)
                )
            }
            let filteredViolations = baseline?.filter(currentViolations) ?? currentViolations
            visitorMutationQueue.sync {
                builder.unfilteredViolations += currentViolations
                builder.violations += filteredViolations
            }

            linter.file.invalidateCache()
            builder.report(violations: filteredViolations, realtimeCondition: true)
        }
    }

    private static func postProcessViolations(
        files: [SwiftLintFile],
        builder: LintOrAnalyzeResultBuilder
    ) throws -> Int {
        let options = builder.options
        let configuration = builder.configuration
        if isWarningThresholdBroken(configuration: configuration, violations: builder.violations), !options.lenient {
            builder.violations.append(
                createThresholdViolation(threshold: configuration.warningThreshold!)
            )
            builder.report(violations: [builder.violations.last!], realtimeCondition: true)
        }
        builder.report(violations: builder.violations, realtimeCondition: false)
        let numberOfSeriousViolations = builder.violations.filter({ $0.severity == .error }).count
        if !options.quiet {
            printStatus(violations: builder.violations, files: files, serious: numberOfSeriousViolations,
                        verb: options.verb)
        }
        if options.benchmark {
            builder.fileBenchmark.save()
            for (id, time) in CustomRuleTimer.shared.dump() {
                builder.ruleBenchmark.record(id: id, time: time)
            }
            builder.ruleBenchmark.save()
            if !options.quiet, let memoryUsage = memoryUsage() {
                queuedPrintError(memoryUsage)
            }
        }
        try builder.cache?.save()
        return numberOfSeriousViolations
    }

    private static func baseline(_ options: LintOrAnalyzeOptions, _ configuration: Configuration) throws -> Baseline? {
        if let baselinePath = options.baseline ?? configuration.baseline {
            do {
                return try Baseline(fromPath: baselinePath)
            } catch {
                Issue.baselineNotReadable(path: baselinePath).print()
                if (error as? CocoaError)?.code != CocoaError.fileReadNoSuchFile ||
                        options.writeBaseline != options.baseline {
                    throw error
                }
            }
        }
        return nil
    }

    private static func printStatus(violations: [StyleViolation], files: [SwiftLintFile], serious: Int, verb: String) {
        let pluralSuffix = { (collection: [Any]) -> String in
            collection.count != 1 ? "s" : ""
        }
        queuedPrintError(
            "Done \(verb)! Found \(violations.count) violation\(pluralSuffix(violations)), " +
            "\(serious) serious in \(files.count) file\(pluralSuffix(files))."
        )
    }

    private static func isWarningThresholdBroken(configuration: Configuration,
                                                 violations: [StyleViolation]) -> Bool {
        guard let warningThreshold = configuration.warningThreshold else { return false }
        let numberOfWarningViolations = violations.filter({ $0.severity == .warning }).count
        return numberOfWarningViolations >= warningThreshold
    }

    private static func createThresholdViolation(threshold: Int) -> StyleViolation {
        let description = RuleDescription(
            identifier: "warning_threshold",
            name: "Warning Threshold",
            description: "Number of warnings thrown is above the threshold",
            kind: .lint
        )
        return StyleViolation(
            ruleDescription: description,
            severity: .error,
            location: Location(file: nil, line: 0, character: 0),
            reason: "Number of warnings exceeded threshold of \(threshold).")
    }

    private static func applyLeniency(
        options: LintOrAnalyzeOptions,
        strict: Bool,
        lenient: Bool,
        violations: [StyleViolation]
    ) -> [StyleViolation] {
        let leniency = options.leniency(strict: strict, lenient: lenient)

        switch leniency {
        case (false, false):
            return violations

        case (false, true):
            return violations.map {
                if $0.severity == .error {
                    return $0.with(severity: .warning)
                }
                return $0
            }

        case (true, false):
            return violations.map {
                if $0.severity == .warning {
                    return $0.with(severity: .error)
                }
                return $0
            }

        case (true, true):
            queuedFatalError("Invalid command line or config options: 'strict' and 'lenient' are mutually exclusive.")
        }
    }

    private static func autocorrect(_ options: LintOrAnalyzeOptions) async throws {
        let storage = RuleStorage()
        let configuration = Configuration(options: options)
        let correctionsBuilder = CorrectionsBuilder()
        let files = try await configuration
            .visitLintableFiles(options: options, cache: nil, storage: storage) { linter in
                if options.format {
                    switch configuration.indentation {
                    case .tabs:
                        linter.format(useTabs: true, indentWidth: 4)
                    case .spaces(let count):
                        linter.format(useTabs: false, indentWidth: count)
                    }
                }

                let corrections = linter.correct(using: storage)
                if !corrections.isEmpty, !options.quiet {
                    if options.useSTDIN {
                        queuedPrint(linter.file.contents)
                    } else {
                        let corrections = corrections.map {
                            Correction(
                                ruleName: $0.0,
                                filePath: linter.file.path,
                                numberOfCorrections: $0.1
                            )
                        }
                        if options.progress {
                            await correctionsBuilder.append(corrections)
                        } else {
                            let correctionLogs = corrections.map(\.consoleDescription)
                            queuedPrint(correctionLogs.joined(separator: "\n"))
                        }
                    }
                }
            }

        if !options.quiet {
            if options.progress {
                let corrections = await correctionsBuilder.corrections
                if !corrections.isEmpty {
                    let correctionLogs = corrections.map(\.consoleDescription)
                    options.writeToOutput(correctionLogs.joined(separator: "\n"))
                }
            }

            let pluralSuffix = { (collection: [Any]) -> String in
                collection.count != 1 ? "s" : ""
            }
            queuedPrintError("Done correcting \(files.count) file\(pluralSuffix(files))!")
        }
    }
}

private class LintOrAnalyzeResultBuilder {
    var fileBenchmark = Benchmark(name: "files")
    var ruleBenchmark = Benchmark(name: "rules")
    /// All detected violations, unfiltered by the baseline, if any.
    var unfilteredViolations = [StyleViolation]()
    /// The violations to be reported, possibly filtered by a baseline, plus any threshold violations.
    var violations = [StyleViolation]()
    let storage = RuleStorage()
    let configuration: Configuration
    let reporter: any Reporter.Type
    let cache: LinterCache?
    let options: LintOrAnalyzeOptions

    init(_ options: LintOrAnalyzeOptions) {
        let config = Signposts.record(name: "LintOrAnalyzeCommand.ParseConfiguration") {
            Configuration(options: options)
        }
        configuration = config
        reporter = reporterFrom(identifier: options.reporter ?? config.reporter)
        if options.ignoreCache || ProcessInfo.processInfo.isLikelyXcodeCloudEnvironment {
            cache = nil
        } else {
            cache = LinterCache(configuration: config)
        }
        self.options = options

        if let outFile = options.output {
            do {
                try Data().write(to: outFile)
            } catch {
                Issue.fileNotWritable(path: outFile).print()
            }
        }
    }

    func report(violations: [StyleViolation], realtimeCondition: Bool) {
        if (reporter.isRealtime && (!options.progress || options.output != nil)) == realtimeCondition {
            let report = reporter.generateReport(violations)
            if !report.isEmpty {
                options.writeToOutput(report)
            }
        }
    }
}

extension LintOrAnalyzeOptions {
    fileprivate func writeToOutput(_ string: String) {
        guard let outFile = output else {
            queuedPrint(string)
            return
        }

        do {
            let fileUpdater = try FileHandle(forUpdating: outFile)
            fileUpdater.seekToEndOfFile()
            fileUpdater.write(Data((string + "\n").utf8))
            fileUpdater.closeFile()
        } catch {
            Issue.fileNotWritable(path: outFile).print()
        }
    }

    typealias Leniency = (strict: Bool, lenient: Bool)

    // Config file settings can be overridden by either `--strict` or `--lenient` command line options.
    func leniency(strict configurationStrict: Bool, lenient configurationLenient: Bool) -> Leniency {
        let strict = strict || (configurationStrict && !lenient)
        let lenient = lenient || (configurationLenient && !self.strict)
        return Leniency(strict: strict, lenient: lenient)
    }
}

private actor CorrectionsBuilder {
    private(set) var corrections: [Correction] = []

    func append(_ corrections: [Correction]) {
        self.corrections.append(contentsOf: corrections)
    }
}

private func memoryUsage() -> String? {
#if os(Linux) || os(Windows)
    return nil
#else
    var info = mach_task_basic_info()
    let basicInfoCount = MemoryLayout<mach_task_basic_info>.stride / MemoryLayout<natural_t>.stride
    var count = mach_msg_type_number_t(basicInfoCount)

    let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
        $0.withMemoryRebound(to: integer_t.self, capacity: basicInfoCount) {
            task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
        }
    }

    if kerr == KERN_SUCCESS {
        let bytes = Measurement<UnitInformationStorage>(value: Double(info.resident_size), unit: .bytes)
        let formatted = ByteCountFormatter().string(from: bytes)
        return "Memory used: \(formatted)"
    }
    let errorMessage = String(cString: mach_error_string(kerr), encoding: .ascii)
    return "Error with task_info(): \(errorMessage ?? "unknown")"
#endif
}
