//
//  ActivePhpInstallation.swift
//  PHP Monitor
//
//  Copyright © 2021 Nico Verbruggen. All rights reserved.
//

import Foundation

/**
 An installed version of PHP, that was detected by scanning the `/opt/php@version/bin` directory.
 
 When initialized, that version's .ini files are also scanned (for active or inactive extensions).
 Integrity checks can be performed to determine whether PHP-FPM is configured correctly.
 
 - Note: Each installation has a separate version number.
 Using `version.short` is advisable if you want to interact with Homebrew.
 */
class ActivePhpInstallation {

    var version: Version!
    var limits: Limits!
    var extensions: [PhpExtension]!
    
    // MARK: - Computed
    
    var formula: String {
        return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)"
    }
    
    // MARK: - Initializer

    init() {
        // Show information about the current version
        getVersion()
        
        // If an error occurred, exit early
        if (version.error) {
            limits = Limits()
            extensions = []
            return
        }
        
        // Load extension information
        let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
        extensions = PhpExtension.load(from: path)
        
        // Get configuration values
        limits = Limits(
            memory_limit: getByteCount(key: "memory_limit"),
            upload_max_filesize: getByteCount(key: "upload_max_filesize"),
            post_max_size: getByteCount(key: "post_max_size")
        )
        
        // Return a list of .ini files parsed after php.ini
        let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"])
            .replacingOccurrences(of: "\n", with: "")
            .split(separator: ",")
            .map { String($0) }
        
        // See if any extensions are present in said .ini files
        paths.forEach { (iniFilePath) in
            let exts = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
            if exts.count > 0 {
                extensions.append(contentsOf: exts)
            }
        }
    }
    
    /**
     When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
     _or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
     */
    private func getVersion() -> Void {
        self.version = Version()
        
        let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
        
        if (version == "" || version.contains("Warning") || version.contains("Error")) {
            self.version.short = "💩 BROKEN"
            self.version.long = ""
            self.version.error = true
            return
        }
        
        // That's the long version
        self.version.long = version
        
        // Next up, let's strip away the minor version number
        let segments = self.version.long.components(separatedBy: ".")
        
        // Get the first two elements
        self.version.short = segments[0...1].joined(separator: ".")
    }
    
    /**
     Retrieves the display value for a specific key in the `.ini` file.
     
     The following values are valid:
     * -1: unlimited (show the infinity icon)
     * 10000: an integer = amount of bytes
     * 1K, 1M, 1G = shorthand for kilobytes, megabytes and gigabytes
     
     If none of these notations are used, the _fallback_ value is used.
     We'll show an emoji to indicate something has gone wrong here.
     To clarify, B gets appended to valid values.
     As a result, "5M" (valid) becomes "5MB", and "5MB" (invalid) becomes ⚠️.
     
     - Parameter key: The key of the `ini` value that needs to be retrieved. For example, you can use `memory_limit`.
     */
    private func getByteCount(key: String) -> String {
        let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
        
        // Check if the value is unlimited
        if (value == "-1") {
            return "∞"
        }
        
        // Check if the syntax is valid otherwise
        let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
        let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first
        return (match == nil) ? "⚠️" : "\(value)B"
    }
    
    /**
     Determine if PHP-FPM is configured correctly.
     
     For PHP 5.6, we'll check if `valet.sock` is included in the main `php-fpm.conf` file, but for more recent
     versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails,
     that means that Valet won't work properly.
     */
    func checkPhpFpmStatus() -> Bool {
        if self.version.short == "5.6" {
            // The main PHP config file should contain `valet.sock` and then we're probably fine?
            let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
            return Shell.pipe("cat \(fileName)").contains("valet.sock")
        }
        
        // Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
        return Shell.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
    }
    
    // MARK: - Structs

    /**
     Struct containing information about the version number of the current PHP installation.
     Also includes information about whether the install is considered "broken" or not.
     If an error was found in the terminal output, `error` is set to `true` and the installation
     can be considered broken. (The app will display this as well.)
     */
    struct Version {
        var short = "???"
        var long = "???"
        var error = false
    }
    
    /**
     Struct containing information about the limits of the current PHP installation.
     Includes: memory limit, max upload size and max post size.
     */
    struct Limits {
        var memory_limit = "???"
        var upload_max_filesize = "???"
        var post_max_size = "???"
    }
    
}
