import Foundation
import StreamCinemaCore

enum CLIError: Error, LocalizedError {
    case missingCredentials
    case invalidCommand

    var errorDescription: String? {
        switch self {
        case .missingCredentials:
            return "Set SC_USERNAME and SC_PASSWORD, or login once in sc-desktop to persist credentials."
        case .invalidCommand:
            return """
            Usage:
              sc-cli menu [path]
              sc-cli play <play-path>
              sc-cli inspect <play-path>
            """
        }
    }
}

@main
struct StreamCinemaCLI {
    static func main() async {
        do {
            try await run()
        } catch {
            fputs("Error: \(error.localizedDescription)\n", stderr)
            exit(1)
        }
    }

    private static func run() async throws {
        let env = ProcessInfo.processInfo.environment
        let args = Array(CommandLine.arguments.dropFirst())
        guard let command = args.first else {
            throw CLIError.invalidCommand
        }

        let authStore = AuthStateStore()
        let persistedState = try await authStore.load()
        let credentials = try resolveCredentials(env: env, persisted: persistedState)
        let username = credentials.username
        let password = credentials.password

        let kra = KraskaClient(configuration: .init(username: username, password: password))
        let streamCinema = StreamCinemaClient(configuration: .init())
        let resolver = PlaybackResolver(streamCinema: streamCinema, kraska: kra)

        var reusedPersistedTokens = false
        if let persistedState, persistedState.username == username, persistedState.password == password {
            if let session = persistedState.kraSessionToken, !session.isEmpty {
                await kra.setSessionID(session)
            }
            if let token = persistedState.streamCinemaAuthToken, !token.isEmpty {
                await streamCinema.setAuthToken(token)
                reusedPersistedTokens = true
            }
        }

        if reusedPersistedTokens {
            print("Using persisted session/token from Keychain")
        } else {
            try await refreshAndPersistAuth(
                kra: kra,
                streamCinema: streamCinema,
                authStore: authStore,
                username: username,
                password: password
            )
        }

        do {
            try await executeCommand(
                command: command,
                args: args,
                streamCinema: streamCinema,
                resolver: resolver
            )
        } catch {
            guard reusedPersistedTokens else { throw error }
            print("Persisted token/session failed, refreshing authentication and retrying once...")
            try await refreshAndPersistAuth(
                kra: kra,
                streamCinema: streamCinema,
                authStore: authStore,
                username: username,
                password: password
            )
            try await executeCommand(
                command: command,
                args: args,
                streamCinema: streamCinema,
                resolver: resolver
            )
        }
    }

    private static func resolveCredentials(
        env: [String: String],
        persisted: PersistedAuthState?
    ) throws -> (username: String, password: String) {
        let envUser = env["SC_USERNAME"]?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        let envPass = env["SC_PASSWORD"]?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""

        if !envUser.isEmpty && !envPass.isEmpty {
            return (envUser, envPass)
        }

        if let persisted, !persisted.username.isEmpty, !persisted.password.isEmpty {
            return (persisted.username, persisted.password)
        }

        throw CLIError.missingCredentials
    }

    private static func refreshAndPersistAuth(
        kra: KraskaClient,
        streamCinema: StreamCinemaClient,
        authStore: AuthStateStore,
        username: String,
        password: String
    ) async throws {
        let sessionID = try await kra.login()
        let authToken = try await streamCinema.fetchAuthToken(kraSessionToken: sessionID)
        try await authStore.save(
            PersistedAuthState(
                username: username,
                password: password,
                kraSessionToken: sessionID,
                streamCinemaAuthToken: authToken
            )
        )
        print("Authenticated. kra session: \(sessionID.prefix(6))..., auth token: \(authToken.prefix(6))...")
    }

    private static func executeCommand(
        command: String,
        args: [String],
        streamCinema: StreamCinemaClient,
        resolver: PlaybackResolver
    ) async throws {

        switch command {
        case "menu":
            let path = args.count > 1 ? args[1] : "/"
            let response = try await streamCinema.get(path: path)
            let items = response.menu ?? []
            print("Menu path \(path) returned \(items.count) items")
            for (index, item) in items.prefix(20).enumerated() {
                print("\(index + 1). [\(item.type ?? "?")] \(item.title ?? "<no title>") -> \(item.url ?? "<no url>")")
            }

        case "play":
            guard args.count > 1 else {
                throw CLIError.invalidCommand
            }
            let playPath = args[1]
            let streams = try await resolver.listStreams(playPath: playPath)
            print("Play path \(playPath) returned \(streams.count) stream candidates")

            for (index, stream) in streams.enumerated() {
                print("\(index + 1). \(stream.quality ?? "?") \(stream.lang ?? "?") bitrate=\(stream.bitrate) provider=\(stream.provider ?? "?")")
            }

            guard let best = streams.max(by: { $0.bitrate < $1.bitrate }) else {
                throw PlaybackResolverError.noStreams
            }
            let finalURL = try await resolver.resolve(stream: best)
            print("Resolved stream URL:\n\(finalURL.absoluteString)")

        case "inspect":
            guard args.count > 1 else {
                throw CLIError.invalidCommand
            }
            let playPath = args[1]
            let response = try await streamCinema.get(path: playPath)
            guard let streams = response.streams, !streams.isEmpty else {
                throw PlaybackResolverError.noStreams
            }
            print("Inspect path \(playPath) returned \(streams.count) stream candidates")
            for (index, stream) in streams.enumerated() {
                print("----- stream \(index + 1) -----")
                print("provider: \(stream.provider ?? "?")")
                print("quality: \(stream.quality ?? "?")")
                print("lang: \(stream.lang ?? "?")")
                print("bitrate: \(stream.bitrate)")
                print("codec: \(stream.videoCodec ?? "?")")
                if let w = stream.videoWidth, let h = stream.videoHeight {
                    print("resolution: \(w)x\(h)")
                }
                print("hdr: \(stream.hasHDR) dv: \(stream.hasDolbyVision) atmos: \(stream.hasAtmos)")
                if let source = stream.source {
                    print("source: \(source)")
                }
                if let group = stream.group {
                    print("group: \(group)")
                }
                if let filename = stream.filename {
                    print("filename: \(filename)")
                }
                print("stream_url: \(stream.url ?? "<none>")")
            }

        default:
            throw CLIError.invalidCommand
        }
    }
}
