import AppKit
import AVKit
import Foundation
import StreamCinemaCore
import SwiftUI
#if canImport(VLCKitSPM)
import VLCKitSPM
#endif

@main
struct StreamCinemaDesktopApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
    @StateObject private var model = DesktopViewModel()

    var body: some Scene {
        WindowGroup("Stream Cinema") {
            ContentView(model: model)
                .frame(minWidth: 1100, minHeight: 700)
        }
    }
}

final class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {
        NSApp.setActivationPolicy(.regular)
        NSApp.activate(ignoringOtherApps: true)
    }
}

struct NativePlayerView: NSViewRepresentable {
    let player: AVPlayer

    func makeNSView(context: Context) -> AVPlayerView {
        let view = AVPlayerView()
        view.controlsStyle = .floating
        view.videoGravity = .resizeAspect
        view.player = player
        return view
    }

    func updateNSView(_ nsView: AVPlayerView, context: Context) {
        if nsView.player !== player {
            nsView.player = player
        }
    }
}

#if canImport(VLCKitSPM)
struct NativeVLCPlayerView: NSViewRepresentable {
    let player: VLCMediaPlayer

    func makeNSView(context: Context) -> VLCVideoView {
        let view = VLCVideoView()
        player.drawable = view
        return view
    }

    func updateNSView(_ nsView: VLCVideoView, context: Context) {
        if (player.drawable as? VLCVideoView) !== nsView {
            player.drawable = nsView
        }
    }
}
#endif

enum PlayerBackend {
    case avPlayer
    case vlc
}

@MainActor
final class DesktopViewModel: ObservableObject {
    @Published var username = ""
    @Published var password = ""
    @Published var isBusy = false
    @Published var status = "Not logged in"

    @Published var currentMenuPath = "/"
    @Published var menuEntries: [MenuEntry] = []
    @Published var streamOptions: [StreamOption] = []
    @Published var selectedStreamIndex: Int?

    @Published var avPlayer: AVPlayer?
    @Published var activePlayerBackend: PlayerBackend = .avPlayer
    @Published var resolvedStreamURL = ""
    @Published var lastPlaybackError = ""

    private let authStore: AuthStateStore
    private var kraskaClient: KraskaClient?
    private var streamCinemaClient: StreamCinemaClient?
    private var playbackResolver: PlaybackResolver?
    private var pathHistory: [String] = ["/"]
    private var avPlayerItemStatusObservation: NSKeyValueObservation?
#if canImport(VLCKitSPM)
    @Published var vlcPlayer: VLCMediaPlayer?
#endif

    init(authStore: AuthStateStore = .init()) {
        self.authStore = authStore
        Task {
            await restoreSessionFromPersistence()
        }
    }

    var canGoBack: Bool {
        pathHistory.count > 1
    }

    func loginAndLoadRoot() async {
        guard !username.isEmpty, !password.isEmpty else {
            status = "Fill username and password."
            return
        }

        await authenticateAndLoadRoot()
    }

    func open(_ entry: MenuEntry) async {
        guard let url = entry.url, !url.isEmpty else {
            status = "Selected item has no URL."
            return
        }

        if url.hasPrefix("/Play/") {
            await loadStreams(playPath: url)
        } else {
            do {
                pathHistory.append(url)
                try await loadMenu(path: url)
            } catch {
                _ = pathHistory.popLast()
                status = "Menu load failed: \(error.localizedDescription)"
            }
        }
    }

    func goBack() async {
        guard canGoBack else { return }
        _ = pathHistory.popLast()
        guard let previous = pathHistory.last else { return }
        do {
            try await loadMenu(path: previous)
        } catch {
            status = "Back navigation failed: \(error.localizedDescription)"
        }
    }

    func resolveAndPlaySelectedStream() async {
        guard
            let selectedStreamIndex,
            streamOptions.indices.contains(selectedStreamIndex),
            let playbackResolver
        else {
            status = "Select a stream first."
            return
        }

        setBusy(true)
        defer { setBusy(false) }

        do {
            let selectedStream = streamOptions[selectedStreamIndex]
            let resolved = try await runWithAuthRetry {
                try await playbackResolver.resolve(stream: selectedStream)
            }
            resolvedStreamURL = resolved.absoluteString
            playWithAVPlayer(url: resolved)
        } catch {
            status = "Playback resolve failed: \(error.localizedDescription)"
        }
    }

    func resolveAndPlaySelectedStreamWithVLC() async {
        guard
            let selectedStreamIndex,
            streamOptions.indices.contains(selectedStreamIndex),
            let playbackResolver
        else {
            status = "Select a stream first."
            return
        }

        setBusy(true)
        defer { setBusy(false) }

        do {
            let selectedStream = streamOptions[selectedStreamIndex]
            let resolved = try await runWithAuthRetry {
                try await playbackResolver.resolve(stream: selectedStream)
            }
            resolvedStreamURL = resolved.absoluteString
            playWithVLC(url: resolved, reason: nil)
        } catch {
            status = "Playback resolve failed: \(error.localizedDescription)"
        }
    }

    func logout() async {
        setBusy(true)
        defer { setBusy(false) }

        do {
            try await authStore.clear()
        } catch {
            status = "Logout warning: \(error.localizedDescription)"
        }

        username = ""
        password = ""
        resolvedStreamURL = ""
        lastPlaybackError = ""
        stopPlayback()
        menuEntries = []
        streamOptions = []
        selectedStreamIndex = nil
        currentMenuPath = "/"
        pathHistory = ["/"]
        kraskaClient = nil
        streamCinemaClient = nil
        playbackResolver = nil
        status = "Logged out"
    }

    private func loadMenu(path: String, showBusy: Bool = true) async throws {
        guard let streamCinemaClient else {
            throw StreamCinemaClientError.missingAuthToken
        }
        if showBusy {
            setBusy(true)
        }
        defer {
            if showBusy {
                setBusy(false)
            }
        }

        let response = try await runWithAuthRetry {
            try await streamCinemaClient.get(path: path)
        }
        currentMenuPath = path
        menuEntries = response.menu ?? []
        streamOptions = []
        selectedStreamIndex = nil
        status = "Loaded \(menuEntries.count) menu items"
    }

    private func loadStreams(playPath: String) async {
        guard let playbackResolver else {
            status = "Not authenticated yet."
            return
        }
        setBusy(true)
        defer { setBusy(false) }

        do {
            currentMenuPath = playPath
            streamOptions = try await runWithAuthRetry {
                try await playbackResolver.listStreams(playPath: playPath)
            }
            selectedStreamIndex = streamOptions.isEmpty ? nil : 0
            status = "Loaded \(streamOptions.count) stream options"
        } catch {
            status = "Stream load failed: \(error.localizedDescription)"
        }
    }

    private func setBusy(_ value: Bool) {
        isBusy = value
    }

    private func playWithAVPlayer(url: URL) {
        stopPlayback()

        let item = AVPlayerItem(url: url)
        avPlayerItemStatusObservation = item.observe(\.status, options: [.initial, .new]) { [weak self] item, _ in
            guard let self else { return }
            Task { @MainActor [weak self] in
                guard let self else { return }
                if item.status == .failed {
                    let reason = item.error?.localizedDescription ?? "unsupported media format"
                    self.lastPlaybackError = reason
                    self.playWithVLC(url: url, reason: reason)
                }
            }
        }

        let player = AVPlayer(playerItem: item)
        avPlayer = player
        activePlayerBackend = .avPlayer
        player.play()
        status = "Playing selected stream (AVPlayer)"
    }

    private func playWithVLC(url: URL, reason: String?) {
#if canImport(VLCKitSPM)
        stopPlayback()
        let media = VLCMedia(url: url)
        let player = VLCMediaPlayer()
        player.media = media
        vlcPlayer = player
        activePlayerBackend = .vlc
        player.play()
        if let reason {
            status = "AVPlayer failed (\(reason)). Switched to VLC fallback."
        } else {
            status = "Playing selected stream (VLC fallback)"
        }
#else
        if let reason {
            status = "AVPlayer failed (\(reason)). VLC fallback unavailable in this build."
        } else {
            status = "VLC fallback unavailable in this build."
        }
#endif
    }

    private func stopPlayback() {
        avPlayerItemStatusObservation = nil
        avPlayer?.pause()
        avPlayer = nil
#if canImport(VLCKitSPM)
        vlcPlayer?.stop()
        vlcPlayer = nil
#endif
    }

    private func authenticateAndLoadRoot() async {
        setBusy(true)
        defer { setBusy(false) }

        do {
            let (kra, sc, resolver) = makeClients()
            let session = try await kra.login()
            let authToken = try await sc.fetchAuthToken(kraSessionToken: session)

            assignClients(kra: kra, sc: sc, resolver: resolver)
            pathHistory = ["/"]
            try await persistAuthState(kraSessionToken: session, streamCinemaAuthToken: authToken)
            try await loadMenu(path: "/", showBusy: false)
            status = "Logged in to kra.sk"
        } catch {
            status = "Login failed: \(error.localizedDescription)"
        }
    }

    private func restoreSessionFromPersistence() async {
        setBusy(true)
        defer { setBusy(false) }

        do {
            guard let state = try await authStore.load() else {
                status = "Not logged in"
                return
            }

            username = state.username
            password = state.password

            guard !username.isEmpty, !password.isEmpty else {
                status = "Saved session is incomplete"
                return
            }

            let (kra, sc, resolver) = makeClients()
            if let session = state.kraSessionToken, !session.isEmpty {
                await kra.setSessionID(session)
            }
            if let authToken = state.streamCinemaAuthToken, !authToken.isEmpty {
                await sc.setAuthToken(authToken)
            }

            assignClients(kra: kra, sc: sc, resolver: resolver)
            pathHistory = ["/"]

            do {
                try await loadMenu(path: "/", showBusy: false)
                status = "Restored saved session"
                return
            } catch {
                let session = try await kra.login()
                let authToken = try await sc.fetchAuthToken(kraSessionToken: session)
                try await persistAuthState(
                    kraSessionToken: session,
                    streamCinemaAuthToken: authToken
                )
                try await loadMenu(path: "/", showBusy: false)
                status = "Session refreshed from saved credentials"
            }
        } catch {
            status = "Failed to restore session: \(error.localizedDescription)"
        }
    }

    private func makeClients() -> (KraskaClient, StreamCinemaClient, PlaybackResolver) {
        let kra = KraskaClient(configuration: .init(username: username, password: password))
        let sc = StreamCinemaClient(configuration: .init())
        let resolver = PlaybackResolver(streamCinema: sc, kraska: kra)
        return (kra, sc, resolver)
    }

    private func assignClients(
        kra: KraskaClient,
        sc: StreamCinemaClient,
        resolver: PlaybackResolver
    ) {
        kraskaClient = kra
        streamCinemaClient = sc
        playbackResolver = resolver
    }

    private func persistAuthState(
        kraSessionToken: String?,
        streamCinemaAuthToken: String?
    ) async throws {
        let state = PersistedAuthState(
            username: username,
            password: password,
            kraSessionToken: kraSessionToken,
            streamCinemaAuthToken: streamCinemaAuthToken
        )
        try await authStore.save(state)
    }

    private func runWithAuthRetry<T>(
        _ operation: () async throws -> T
    ) async throws -> T {
        do {
            return try await operation()
        } catch {
            guard isAuthorizationError(error) else {
                throw error
            }
            try await refreshAuthTokens()
            return try await operation()
        }
    }

    private func refreshAuthTokens() async throws {
        guard !username.isEmpty, !password.isEmpty else {
            throw StreamCinemaClientError.missingAuthToken
        }
        let clients = ensureClients()
        let session = try await clients.kra.login()
        let authToken = try await clients.sc.fetchAuthToken(kraSessionToken: session)
        try await persistAuthState(kraSessionToken: session, streamCinemaAuthToken: authToken)
        status = "Session refreshed, retrying request..."
    }

    private func ensureClients() -> (
        kra: KraskaClient,
        sc: StreamCinemaClient,
        resolver: PlaybackResolver
    ) {
        if let kra = kraskaClient, let sc = streamCinemaClient, let resolver = playbackResolver {
            return (kra, sc, resolver)
        }
        let (kra, sc, resolver) = makeClients()
        assignClients(kra: kra, sc: sc, resolver: resolver)
        return (kra, sc, resolver)
    }

    private func isAuthorizationError(_ error: Error) -> Bool {
        if let httpError = error as? HTTPClientError {
            switch httpError {
            case .badStatus(let code, let body):
                if code == 401 {
                    return true
                }
                return body.lowercased().contains("authorization required")
            case .invalidResponse:
                return false
            }
        }

        let message = error.localizedDescription.lowercased()
        return message.contains("http 401") || message.contains("authorization required")
    }
}

struct ContentView: View {
    @ObservedObject var model: DesktopViewModel

    var body: some View {
        if model.streamCinemaClientReady {
            desktopBody
        } else {
            loginBody
        }
    }

    private var loginBody: some View {
        VStack(spacing: 16) {
            Text("Stream Cinema")
                .font(.system(size: 34, weight: .bold))
            Text("Native macOS baseline (login -> menu -> stream -> play)")
                .foregroundStyle(.secondary)

            VStack(spacing: 10) {
                TextField("Kra username", text: $model.username)
                    .textFieldStyle(.roundedBorder)
                SecureField("Kra password", text: $model.password)
                    .textFieldStyle(.roundedBorder)
            }
            .frame(maxWidth: 420)

            Button {
                Task { await model.loginAndLoadRoot() }
            } label: {
                if model.isBusy {
                    ProgressView()
                } else {
                    Text("Login")
                }
            }
            .buttonStyle(.borderedProminent)
            .disabled(model.isBusy)

            Text(model.status)
                .font(.footnote)
                .foregroundStyle(.secondary)
        }
        .padding(32)
    }

    private var desktopBody: some View {
        VStack(spacing: 0) {
            HStack {
                Button("Back") {
                    Task { await model.goBack() }
                }
                .disabled(!model.canGoBack || model.isBusy)

                Button("Logout") {
                    Task { await model.logout() }
                }
                .disabled(model.isBusy)

                Text("Path: \(model.currentMenuPath)")
                    .font(.footnote.monospaced())
                    .lineLimit(1)

                Spacer()
                Text(model.status)
                    .font(.footnote)
                    .foregroundStyle(.secondary)
            }
            .padding(.horizontal, 14)
            .padding(.vertical, 10)

            Divider()

            HSplitView {
                List(Array(model.menuEntries.enumerated()), id: \.offset) { _, entry in
                    Button {
                        Task { await model.open(entry) }
                    } label: {
                        VStack(alignment: .leading, spacing: 2) {
                            Text(entry.title ?? "<no title>")
                            Text(entry.url ?? "<no url>")
                                .font(.caption)
                                .foregroundStyle(.secondary)
                        }
                    }
                    .buttonStyle(.plain)
                }
                .frame(minWidth: 340)

                VStack(spacing: 0) {
                    List(Array(model.streamOptions.enumerated()), id: \.offset) { index, stream in
                        Button {
                            model.selectedStreamIndex = index
                        } label: {
                            HStack {
                                VStack(alignment: .leading, spacing: 2) {
                                    Text("\(stream.quality ?? "?")  \(stream.lang ?? "?")  \(stream.provider ?? "?")")
                                    Text("bitrate=\(stream.bitrate)")
                                        .font(.caption)
                                        .foregroundStyle(.secondary)
                                }
                                Spacer()
                                if model.selectedStreamIndex == index {
                                    Image(systemName: "checkmark.circle.fill")
                                        .foregroundStyle(.green)
                                }
                            }
                        }
                        .buttonStyle(.plain)
                    }
                    .frame(minHeight: 220)

                    HStack {
                        Button("Resolve + Play") {
                            Task { await model.resolveAndPlaySelectedStream() }
                        }
                        .buttonStyle(.borderedProminent)
                        .disabled(model.selectedStreamIndex == nil || model.isBusy)

                        Button("Play with VLC") {
                            Task { await model.resolveAndPlaySelectedStreamWithVLC() }
                        }
                        .disabled(model.selectedStreamIndex == nil || model.isBusy || !model.vlcFallbackAvailable)

                        Spacer()
                        Text(model.resolvedStreamURL)
                            .font(.caption.monospaced())
                            .lineLimit(1)
                            .foregroundStyle(.secondary)
                    }
                    .padding(10)

                    if model.activePlayerBackend == .vlc {
#if canImport(VLCKitSPM)
                        if let player = model.vlcPlayer {
                            NativeVLCPlayerView(player: player)
                                .frame(minHeight: 320)
                        } else {
                            placeholderPlayer
                        }
#else
                        placeholderPlayer
#endif
                    } else if let player = model.avPlayer {
                        NativePlayerView(player: player)
                            .frame(minHeight: 320)
                    } else {
                        placeholderPlayer
                    }
                }
            }
        }
    }

    private var placeholderPlayer: some View {
        ZStack {
            Rectangle().fill(Color.black.opacity(0.92))
            VStack(spacing: 8) {
                Text("Player")
                    .foregroundStyle(.white.opacity(0.5))
                if !model.lastPlaybackError.isEmpty {
                    Text(model.lastPlaybackError)
                        .font(.caption)
                        .foregroundStyle(.white.opacity(0.75))
                        .lineLimit(2)
                }
            }
        }
        .frame(minHeight: 320)
    }
}

private extension DesktopViewModel {
    var streamCinemaClientReady: Bool {
        streamCinemaClient != nil && playbackResolver != nil && kraskaClient != nil
    }

    var vlcFallbackAvailable: Bool {
#if canImport(VLCKitSPM)
        true
#else
        false
#endif
    }
}
