import AppKit
import Foundation

enum BlitztextInstallLocationService {
    enum InstallLocation: Equatable {
        case applications
        case userApplications
        case outsideApplications(URL)
        case unknown

        var isInApplicationsFolder: Bool {
            switch self {
            case .applications, .userApplications:
                return true
            case .outsideApplications, .unknown:
                return false
            }
        }

        var isCanonicalInstall: Bool {
            if case .applications = self {
                return true
            }
            return false
        }
    }

    enum MoveError: LocalizedError {
        case sourceBundleMissing(URL)
        case destinationUnavailable
        case destinationExists(URL)
        case destinationNotWritable(URL)
        case copyFailed(source: URL, destination: URL, underlying: Error)

        var errorDescription: String? {
            switch self {
            case .sourceBundleMissing:
                return "Die aktuelle App-Installation wurde nicht gefunden."
            case .destinationUnavailable:
                return "Der Zielordner /Applications ist nicht verfügbar."
            case .destinationExists:
                return "Am Zielort liegt bereits eine Blitztext-Installation."
            case .destinationNotWritable:
                return "Der Zielordner /Applications ist auf diesem Mac nicht beschreibbar."
            case .copyFailed:
                return "Blitztext konnte nicht nach /Applications kopiert werden."
            }
        }
    }

    static var bundleURL: URL {
        Bundle.main.bundleURL.standardizedFileURL.resolvingSymlinksInPath()
    }

    static var bundleName: String {
        bundleURL.deletingPathExtension().lastPathComponent
    }

    static var currentInstallLocation: InstallLocation {
        let currentDirectory = bundleURL.deletingLastPathComponent()
        let standardizedCurrentDirectory = currentDirectory.standardizedFileURL.resolvingSymlinksInPath()

        if standardizedCurrentDirectory == systemApplicationsDirectoryURL.standardizedFileURL.resolvingSymlinksInPath() {
            return .applications
        }

        if standardizedCurrentDirectory == userApplicationsDirectoryURL.standardizedFileURL.resolvingSymlinksInPath() {
            return .userApplications
        }

        if FileManager.default.fileExists(atPath: bundleURL.path) {
            return .outsideApplications(bundleURL)
        }

        return .unknown
    }

    static var shouldOfferMoveToApplications: Bool {
        !currentInstallLocation.isCanonicalInstall
    }

    static var systemApplicationsDirectoryURL: URL {
        URL(fileURLWithPath: "/Applications", isDirectory: true)
    }

    static var userApplicationsDirectoryURL: URL {
        FileManager.default.homeDirectoryForCurrentUser
            .appendingPathComponent("Applications", isDirectory: true)
    }

    static var preferredInstallDirectoryURL: URL? {
        systemApplicationsDirectoryURL
    }

    static var preferredInstallBundleURL: URL? {
        preferredInstallDirectoryURL?.appendingPathComponent(bundleURL.lastPathComponent)
    }

    static var knownInstallBundleURLs: [URL] {
        let candidates = [
            bundleURL,
            systemApplicationsDirectoryURL.appendingPathComponent(bundleURL.lastPathComponent),
            userApplicationsDirectoryURL.appendingPathComponent(bundleURL.lastPathComponent)
        ]

        var seen = Set<String>()
        return candidates.filter { candidate in
            let key = candidate.standardizedFileURL.resolvingSymlinksInPath().path
            guard seen.insert(key).inserted else { return false }
            return FileManager.default.fileExists(atPath: candidate.path)
        }
    }

    static var otherInstalledBundleURLs: [URL] {
        knownInstallBundleURLs.filter { $0 != bundleURL }
    }

    static func moveCurrentAppToApplications(replacingExisting: Bool = true) throws -> URL {
        let sourceURL = bundleURL
        guard FileManager.default.fileExists(atPath: sourceURL.path) else {
            throw MoveError.sourceBundleMissing(sourceURL)
        }

        guard let destinationDirectoryURL = preferredInstallDirectoryURL else {
            throw MoveError.destinationUnavailable
        }

        let destinationURL = destinationDirectoryURL.appendingPathComponent(sourceURL.lastPathComponent)

        if sourceURL.standardizedFileURL.resolvingSymlinksInPath() == destinationURL.standardizedFileURL.resolvingSymlinksInPath() {
            return destinationURL
        }

        try ensureDirectoryExists(at: destinationDirectoryURL)

        if FileManager.default.fileExists(atPath: destinationURL.path) {
            guard replacingExisting else {
                throw MoveError.destinationExists(destinationURL)
            }
            try FileManager.default.removeItem(at: destinationURL)
        }

        guard FileManager.default.isWritableFile(atPath: destinationDirectoryURL.path) else {
            throw MoveError.destinationNotWritable(destinationDirectoryURL)
        }

        do {
            try FileManager.default.copyItem(at: sourceURL, to: destinationURL)
        } catch {
            throw MoveError.copyFailed(source: sourceURL, destination: destinationURL, underlying: error)
        }

        return destinationURL
    }

    static func moveToApplicationsAndRelaunch(replacingExisting: Bool = true) throws {
        let destinationURL = try moveCurrentAppToApplications(replacingExisting: replacingExisting)

        let openProcess = Process()
        openProcess.executableURL = URL(fileURLWithPath: "/usr/bin/open")
        openProcess.arguments = [destinationURL.path]
        try openProcess.run()

        DispatchQueue.main.async {
            NSApplication.shared.terminate(nil)
        }
    }

    private static func ensureDirectoryExists(at url: URL) throws {
        if FileManager.default.fileExists(atPath: url.path) {
            return
        }

        try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
    }
}
