import APIClient
import Combine
import ComponentLibrary
import ComposableArchitecture
import Foundation
import GenAPI
import OSLog
import ShareAssetClient
import Utilities

private let log = Logger(category: "HookDownloadClient")

public enum HookDownloadError: LocalizedError, Equatable {
    case noData
    case timedOut(hookId: String, pollingAttempts: Int)
    case pollingFailed(hookId: String, underlyingError: String)
    case invalidDownloadURL(hookId: String, urlString: String?)
    case invalidHTTPResponse(hookId: String, url: String)
    case httpStatusError(hookId: String, statusCode: Int, url: String)
    case networkError(hookId: String, underlyingError: String)
    case fileSystemError(hookId: String, operation: String, underlyingError: String)
    case photoLibraryError(hookId: String, underlyingError: String)
    case cancelled(hookId: String)
    case unknown(hookId: String, underlyingError: String)

    public var errorDescription: String? {
        switch self {
        case .noData:
            return "No data received"
        case let .timedOut(hookId, pollingAttempts):
            return "Timed out after \(pollingAttempts) polling attempts for hook \(hookId)"
        case let .pollingFailed(hookId, underlyingError):
            return "Failed to poll download URL for hook \(hookId): \(underlyingError)"
        case let .invalidDownloadURL(hookId, urlString):
            return "Invalid download URL for hook \(hookId): \(urlString ?? "nil")"
        case let .invalidHTTPResponse(hookId, url):
            return "Invalid HTTP response for hook \(hookId) at URL: \(url)"
        case let .httpStatusError(hookId, statusCode, url):
            return "HTTP error \(statusCode) for hook \(hookId) at URL: \(url)"
        case let .networkError(hookId, underlyingError):
            return "Network error for hook \(hookId): \(underlyingError)"
        case let .fileSystemError(hookId, operation, underlyingError):
            return "File system error during \(operation) for hook \(hookId): \(underlyingError)"
        case let .photoLibraryError(hookId, underlyingError):
            return "Photo library error for hook \(hookId): \(underlyingError)"
        case let .cancelled(hookId):
            return "Download cancelled for hook \(hookId)"
        case let .unknown(hookId, underlyingError):
            return "Unknown error for hook \(hookId): \(underlyingError)"
        }
    }
}

public enum HookDownloadClientStatus: Equatable {
    case fetchingDownloadURL
    case successfullyFetchedDownloadURL(URL)
    case downloadingHook
    case successfullyDownloadedHook
    case failure(HookDownloadError)
}

public struct HookDownloadClientTask: Equatable, Identifiable {
    public let id: String
    public let type: TaskType
    public var status: HookDownloadClientStatus

    public enum TaskType: Equatable {
        case fetchDownloadURL(hookId: String)
        case downloadHook(hookId: String, url: URL)

        var processingStatus: HookDownloadClientStatus {
            switch self {
            case .fetchDownloadURL:
                return .fetchingDownloadURL
            case .downloadHook:
                return .downloadingHook
            }
        }
    }

    public init(
        id: String = UUID().uuidString,
        type: TaskType,
        status: HookDownloadClientStatus
    ) {
        self.id = id
        self.type = type
        self.status = status
    }
}

@DependencyClient
public struct HookDownloadClient {
    public enum ShareTarget: Equatable {
        case download
        case instagram
        case tiktok
        case facebook
    }

    public enum HookDownloadClientEvent: Equatable {
        case startedFetchingDownloadURL(_ hook: Hook, _ taskId: String, _ target: ShareTarget, _ attributionURL: URL?)
        case polledForDownloadURL(_ hook: Hook, _ taskId: String, _ pollingCount: Int, _ target: ShareTarget, _ attributionURL: URL?)
        case downloadURLReady(_ hook: Hook, _ taskId: String, _ downloadURL: URL, _ target: ShareTarget, _ attributionURL: URL?)
        case startedDownloadingHook(_ hook: Hook, _ taskId: String, _ downloadURL: URL, _ target: ShareTarget, _ attributionURL: URL?)
        case hookDownloadComplete(_ hook: Hook, _ taskId: String, _ localURL: URL, _ target: ShareTarget, _ attributionURL: URL?)
        case hookDownloadFailed(_ hook: Hook, _ taskId: String, _ error: HookDownloadError, _ target: ShareTarget)
    }

    public var stream: () -> AsyncStream<HookDownloadClientEvent> = { .never }
    public var triggerHookDownload: (_ hook: Hook, _ target: ShareTarget, _ attributionURL: URL?) -> Void = { _, _, _ in }
}

extension HookDownloadClient: DependencyKey {
    public static var liveValue: HookDownloadClient {
        @Dependency(APIClientV2.self) var api
        @Dependency(\.continuousClock) var clock

        let subject = PassthroughSubject<HookDownloadClientEvent, Never>()
        let timeout: TimeInterval = 30
        let pollingInterval: TimeInterval = 3.0
        let resourceCache = ShareAssetResourceCache()

        return Self(
            stream: {
                UncheckedSendable(subject.values).eraseToStream()
            },
            triggerHookDownload: { hook, target, attributionURL in
                let taskId = UUID().uuidString

                if let cachedURL = resourceCache.getCachedFile(for: .init(hook.id, type: .mp4)) {
                    log.info("Using cached Hook video - hookId: \(hook.id)")
                    subject.send(.hookDownloadComplete(hook, taskId, cachedURL, target, attributionURL))
                    return
                }

                Task {
                    do {
                        subject.send(.startedFetchingDownloadURL(hook, taskId, target, attributionURL))

                        // Poll for download URL (max 30 seconds)
                        let startTime = Date()
                        var pollingCount = 0
                        var downloadURL: URL?

                        while Date().timeIntervalSince(startTime) < timeout, downloadURL == nil {
                            subject.send(.polledForDownloadURL(hook, taskId, pollingCount, target, attributionURL))
                            pollingCount += 1

                            do {
                                let response = try await api.getHookDownloadURL(hook.id)
                                if let urlString = response.downloadUrl, !urlString.isEmpty {
                                    guard let url = URL(string: urlString) else {
                                        let error = HookDownloadError.invalidDownloadURL(hookId: hook.id, urlString: urlString)
                                        log.telemetry.error(error, message: "Received invalid download URL from API - hookId: \(hook.id)")
                                        throw error
                                    }
                                    downloadURL = url
                                    break
                                }
                            } catch let downloadError as HookDownloadError {
                                // Re-throw our custom errors
                                throw downloadError
                            } catch {
                                // Log API polling errors but continue polling
                                let wrappedError = HookDownloadError.pollingFailed(hookId: hook.id, underlyingError: String(describing: error))
                                log.telemetry.error(wrappedError, message: "Polling attempt \(pollingCount) failed - hookId: \(hook.id)")
                                // Don't throw - continue polling until timeout
                            }

                            // Wait before next poll
                            try await clock.sleep(for: .seconds(pollingInterval))
                        }

                        guard let downloadURL = downloadURL else {
                            let error = HookDownloadError.timedOut(hookId: hook.id, pollingAttempts: pollingCount)
                            log.telemetry.error(error, message: "Timed out waiting for download URL - hookId: \(hook.id)")
                            subject.send(.hookDownloadFailed(hook, taskId, error, target))
                            return
                        }

                        subject.send(.downloadURLReady(hook, taskId, downloadURL, target, attributionURL))

                        // Download the actual video file and cache it
                        subject.send(.startedDownloadingHook(hook, taskId, downloadURL, target, attributionURL))

                        let cachedLocalURL: URL
                        do {
                            cachedLocalURL = try await resourceCache.downloadAndSetCacheResource(
                                for: .init(hook.id, type: .mp4),
                                from: downloadURL
                            )
                            log.info("Successfully downloaded and cached hook video - hookId: \(hook.id)")
                        } catch {
                            let wrappedError = HookDownloadError.networkError(hookId: hook.id, underlyingError: String(describing: error))
                            log.telemetry.error(wrappedError, message: "Failed to download and cache hook video - hookId: \(hook.id)")
                            throw wrappedError
                        }

                        // Save to Photos
                        let videoDownloader = VideoDownloader()
                        do {
                            _ = try await videoDownloader.saveVideo(from: cachedLocalURL)
                            log.info("Successfully saved hook video to Photos - hookId: \(hook.id)")
                        } catch {
                            let wrappedError = HookDownloadError.photoLibraryError(
                                hookId: hook.id,
                                underlyingError: String(describing: error)
                            )
                            log.telemetry.error(wrappedError, message: "Failed to save video to photo library - hookId: \(hook.id)")
                            throw wrappedError
                        }

                        subject.send(.hookDownloadComplete(hook, taskId, cachedLocalURL, target, attributionURL))

                    } catch let hookError as HookDownloadError {
                        log.telemetry.error(hookError, message: "Hook download failed with known error - hookId: \(hook.id)")
                        subject.send(.hookDownloadFailed(hook, taskId, hookError, target))
                    } catch is CancellationError {
                        let hookError = HookDownloadError.cancelled(hookId: hook.id)
                        log.telemetry.error(hookError, message: "Hook download was cancelled - hookId: \(hook.id)")
                        subject.send(.hookDownloadFailed(hook, taskId, hookError, target))
                    } catch {
                        let hookError = HookDownloadError.unknown(hookId: hook.id, underlyingError: String(describing: error))
                        log.telemetry.error(hookError, message: "Hook download failed with unexpected error - hookId: \(hook.id)")
                        subject.send(.hookDownloadFailed(hook, taskId, hookError, target))
                    }
                }
            }
        )
    }
}

public extension DependencyValues {
    var hookDownloadClient: HookDownloadClient {
        get { self[HookDownloadClient.self] }
        set { self[HookDownloadClient.self] = newValue }
    }
}
