import Foundation

/// Wrapper um `ideviceinfo` aus libimobiledevice.
/// Liest die wichtigsten Lockdown-Keys eines per USB verbundenen iPhones.
enum DeviceDetector {
    enum DetectorError: Error, LocalizedError {
        case ideviceinfoMissing
        case cfgutilMissing
        case noDevice
        case deviceLocked
        case profileUserInteractionRequired
        case profileInstallRequiresManagementTool
        case parseError(String)

        var errorDescription: String? {
            switch self {
            case .ideviceinfoMissing:
                return "ideviceinfo nicht gefunden — bitte `brew install libimobiledevice` ausführen."
            case .cfgutilMissing:
                return "cfgutil nicht gefunden — bitte Apple Configurator installieren."
            case .noDevice:
                return "Kein iPhone via USB erkannt. Kabel + Trust-Dialog am iPhone prüfen."
            case .deviceLocked:
                return "iPhone ist gesperrt. Bitte entsperren und USB verbunden lassen."
            case .profileUserInteractionRequired:
                return "iOS verlangt eine Bestätigung direkt am iPhone, um das Profil zu installieren."
            case .profileInstallRequiresManagementTool:
                return "Lokale Profil-Installation ist durch iOS-Policy blockiert (DMC 4020). Dieses Profil muss per MDM-Command installiert werden oder per AirDrop/User-Flow bestätigt werden."
            case .parseError(let msg):
                return "Parse-Fehler: \(msg)"
            }
        }
    }

    static func detect() async throws -> DeviceState {
        guard let bin = Paths.firstExecutable(in: Paths.ideviceinfoCandidates) else {
            throw DetectorError.ideviceinfoMissing
        }

        async let udid     = readKey(bin: bin, key: "UniqueDeviceID")
        async let model    = readKey(bin: bin, key: "ProductType")
        async let version  = readKey(bin: bin, key: "ProductVersion")
        async let name     = readKey(bin: bin, key: "DeviceName")

        let (u, m, v, n) = try await (udid, model, version, name)
        guard !u.isEmpty else { throw DetectorError.noDevice }

        return DeviceState(
            udid: u,
            productType: m,
            productVersion: v,
            deviceName: n.isEmpty ? "iPhone" : n
        )
    }

    private static func readKey(bin: String, key: String) async throws -> String {
        let r = try await ProcessRunner.run(bin, arguments: ["-k", key])
        if r.exitCode != 0 {
            // ideviceinfo gibt non-zero zurück wenn kein Device da ist
            if r.stderr.contains("No device found") || r.stderr.contains("ERROR") {
                throw DetectorError.noDevice
            }
            throw DetectorError.parseError(r.stderr)
        }
        return r.stdout.trimmingCharacters(in: .whitespacesAndNewlines)
    }

    // MARK: - Supervision status via supervise-magic

    struct SupervisionStatus: Equatable {
        var isSupervised: Bool
        var organizationName: String?
        var findMyEnabled: Bool?

        /// Wir betrachten das Gerät als "schon durch uns gebunden" wenn
        /// OrganizationName matched. Case-insensitive für Robustheit.
        var isOwnedByReBreak: Bool {
            isSupervised && (normalizedOrganizationName?.localizedCaseInsensitiveCompare("ReBreak") == .orderedSame)
        }

        var normalizedOrganizationName: String? {
            organizationName?
                .trimmingCharacters(in: .whitespacesAndNewlines)
                .trimmingCharacters(in: CharacterSet(charactersIn: "\"'"))
        }
    }

    /// Liest IsSupervised + OrganizationName via `supervise-magic cloud-config`.
    /// Falls iPhone nicht ansprechbar oder unsupervised → returnt nil-felder.
    static func readSupervisionStatus() async -> SupervisionStatus {
        guard let bin = Paths.firstExecutable(in: Paths.superviseMagicCandidates) else {
            return SupervisionStatus(isSupervised: false, organizationName: nil, findMyEnabled: nil)
        }
        var status = SupervisionStatus(isSupervised: false, organizationName: nil, findMyEnabled: nil)

        // 1) cloud-config liest IsSupervised + OrganizationName direkt aus MCInstall.
        if let r = try? await ProcessRunner.run(bin, arguments: ["cloud-config"]), r.exitCode == 0 {
            for raw in r.stdout.split(separator: "\n") {
                let line = String(raw)
                if let v = parseEquals(line: line, key: "IsSupervised") {
                    status.isSupervised = (v.lowercased() == "true")
                }
                if let v = parseEquals(line: line, key: "OrganizationName") {
                    status.organizationName = normalizeOrgName(v)
                }
            }
        }

        // 2) `check` gibt zusätzlich FindMyEnabled.
        if let r = try? await ProcessRunner.run(bin, arguments: ["check"]), !r.stdout.isEmpty {
            for raw in r.stdout.split(separator: "\n") {
                let line = String(raw)
                if let v = parseColon(line: line, key: "FindMyEnabled") {
                    status.findMyEnabled = (v.lowercased() == "true")
                }
                if status.isSupervised == false, let v = parseColon(line: line, key: "IsSupervised") {
                    status.isSupervised = (v.lowercased() == "true")
                }
                if status.organizationName == nil,
                   let v = parseColon(line: line, key: "OrganizationName") ?? parseColon(line: line, key: "SupervisionOrganizationName") {
                    status.organizationName = normalizeOrgName(v)
                }
            }
        }
        return status
    }

    private static func normalizeOrgName(_ value: String) -> String {
        value
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .trimmingCharacters(in: CharacterSet(charactersIn: "\"'"))
    }

    /// Parse `  Key                 = Value` (cloud-config Format).
    private static func parseEquals(line: String, key: String) -> String? {
        let trimmed = line.trimmingCharacters(in: .whitespaces)
        guard trimmed.hasPrefix(key) else { return nil }
        let parts = trimmed.split(separator: "=", maxSplits: 1).map { $0.trimmingCharacters(in: .whitespaces) }
        guard parts.count == 2, parts[0] == key else { return nil }
        return parts[1]
    }

    /// Parse `  Key:    Value` (check Format).
    private static func parseColon(line: String, key: String) -> String? {
        let trimmed = line.trimmingCharacters(in: .whitespaces)
        guard trimmed.hasPrefix(key + ":") else { return nil }
        let after = trimmed.dropFirst(key.count + 1)
        return after.trimmingCharacters(in: .whitespaces)
    }

    // MARK: - Installed configuration profiles via cfgutil

    /// Listet alle PayloadIdentifiers der aktuell auf dem iPhone installierten
    /// Configuration-Profiles. cfgutil-output-Format: "<identifier> <version> <displayName>".
    /// Returnt leeres Array wenn cfgutil fehlt oder Device nicht ansprechbar.
    static func installedProfileIDs() async -> [String] {
        guard let cfgutil = Paths.cfgutilPath else { return [] }
        guard let r = try? await ProcessRunner.run(cfgutil, arguments: ["--foreach", "get", "configurationProfiles"]),
              r.exitCode == 0 else { return [] }
        // Format: "org.rebreak.mdm.enrollment ReBreak MDM v1" (1 line per profile)
        return r.stdout
            .split(separator: "\n")
            .compactMap { line -> String? in
                let trimmed = line.trimmingCharacters(in: .whitespaces)
                guard !trimmed.isEmpty else { return nil }
                // erstes Token = identifier
                return trimmed.split(separator: " ", maxSplits: 1).first.map(String.init)
            }
    }

    /// Listet alle installierten App-Bundle-IDs.
    /// Format: "com.bundle.id\tDisplayName (CFBundleName) vBuildNumber".
    /// Hinweis: cfgutil hat keinen direkten "managed?"-Indikator pro App —
    /// managed-status muss via MDM-`ManagedApplicationList`-Command geprüft werden.
    static func installedAppBundleIDs() async -> [String] {
        guard let cfgutil = Paths.cfgutilPath else { return [] }
        guard let r = try? await ProcessRunner.run(cfgutil, arguments: ["--foreach", "get", "installedApps"]),
              r.exitCode == 0 else { return [] }
        return r.stdout
            .split(separator: "\n")
            .compactMap { line -> String? in
                let trimmed = line.trimmingCharacters(in: .whitespaces)
                guard !trimmed.isEmpty else { return nil }
                // erstes Token (vor TAB oder Space) = bundle-id
                return trimmed.split(whereSeparator: { $0 == "\t" || $0 == " " }).first.map(String.init)
            }
    }

    /// Versucht ein .mobileconfig direkt auf ein per USB verbundenes iPhone zu
    /// installieren. Nutzt cfgutil und ist damit ohne AirDrop-Dialog möglich,
    /// sofern Device trusted/entsperrt ist.
    static func installProfileSilently(path: String) async throws {
        guard let cfgutil = Paths.cfgutilPath else {
            throw DetectorError.cfgutilMissing
        }
        let r = try await ProcessRunner.run(cfgutil, arguments: ["--foreach", "install-profile", path])
        if r.exitCode != 0 {
            let err = r.stderr.isEmpty ? r.stdout : r.stderr
            if err.localizedCaseInsensitiveContains("device is locked") {
                throw DetectorError.deviceLocked
            }
            if err.localizedCaseInsensitiveContains("benutzerinteraktion")
                || err.localizedCaseInsensitiveContains("user interaction")
                || err.contains("MCInstallationErrorDomain Code: 4009") {
                throw DetectorError.profileUserInteractionRequired
            }
            if err.contains("DMCInstallationErrorDomain") && err.contains("Code: 4020") {
                throw DetectorError.profileInstallRequiresManagementTool
            }
            throw DetectorError.parseError(err.trimmingCharacters(in: .whitespacesAndNewlines))
        }
    }

    /// Entfernt eine App per Bundle-ID via cfgutil (USB).
    static func removeApp(bundleID: String) async throws {
        guard let cfgutil = Paths.cfgutilPath else {
            throw DetectorError.cfgutilMissing
        }
        let r = try await ProcessRunner.run(cfgutil, arguments: ["--foreach", "remove-app", bundleID])
        if r.exitCode != 0 {
            let err = r.stderr.isEmpty ? r.stdout : r.stderr
            throw DetectorError.parseError(err.trimmingCharacters(in: .whitespacesAndNewlines))
        }
    }

    /// Entfernt alle per Identifier angegebenen Profile via cfgutil.
    /// Wird für interne Test-Resets genutzt.
    static func removeProfiles(identifiers: [String]) async throws {
        guard let cfgutil = Paths.cfgutilPath else {
            throw DetectorError.cfgutilMissing
        }
        for identifier in identifiers {
            let r = try await ProcessRunner.run(cfgutil, arguments: ["--foreach", "remove-profile", identifier])
            if r.exitCode != 0 {
                let err = r.stderr.isEmpty ? r.stdout : r.stderr
                throw DetectorError.parseError(err.trimmingCharacters(in: .whitespacesAndNewlines))
            }
        }
    }

    /// Internal QA helper: entfernt alle Profile mit `org.rebreak.` Prefix.
    /// Returnt die tatsächlich angezielten Profil-IDs.
    static func removeAllReBreakProfiles() async throws -> [String] {
        let profileIDs = await installedProfileIDs().filter { $0.hasPrefix("org.rebreak.") }
        guard !profileIDs.isEmpty else { return [] }
        try await removeProfiles(identifiers: profileIDs)
        return profileIDs
    }
}
