import Foundation

enum ShareAssetResourceError: Error {
    case couldNotSetupResourceCacheDirectory
    case couldNotCreateSourceforResourceCache
    case couldNotClearCacheItem
    case couldNotDownloadItem
    case noCacheRootDirectorySet
}

/* Adapted from Adamantium Resource Cache */

public struct ShareAssetResourceCache {
    // Define unique cache keys
    public struct CacheKey: Hashable, Equatable {
        public enum ResourceType: String {
            case mp4
        }

        let resourceType: ResourceType
        let resourceKey: String
        public init(_ key: String, type: ResourceType) {
            // a...z, A...Z, 0...9, - and _
            // Initialize with the valid rawValue
            resourceKey = key
            resourceType = type
        }

        var resourceTag: String {
            let adjustedKey: String
            switch resourceType {
            case .mp4:
                adjustedKey = "videomp4-\(resourceKey)"
            }
            return adjustedKey
        }

        var isCachedKey: String {
            return "shareasset-is-cached-\(resourceTag)"
        }

        var filenameWithoutExtension: String {
            return "shareasset-cache-file-\(resourceTag)"
        }

        var fileExtension: String {
            switch resourceType {
            case .mp4:
                return "mp4"
            }
        }

        func fileURL(_ rootURL: URL) -> URL {
            return rootURL
                .appendingPathComponent(filenameWithoutExtension)
                .appendingPathExtension(fileExtension)
        }
    }

    static var cacheParentURL: URL? {
        return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
    }

    private let rootDirectory: URL?
    public init(_ rootDirectory: URL? = nil) {
        self.rootDirectory = rootDirectory ?? ShareAssetResourceCache.cacheParentURL
    }

    public func setupShareAssetResourceCacheIfNeeded() throws {
        guard let directoryURL = cacheDirectoryURL else { return }
        guard
            !FileManager.default.fileExists(
                atPath: directoryURL.path,
                isDirectory: nil
            )
        else { return }
        do {
            try FileManager.default.createDirectory(
                at: directoryURL,
                withIntermediateDirectories: true
            )
        } catch {
            print("ShareAsset: Error creating cache directory: \(error)")
            throw ShareAssetResourceError.couldNotSetupResourceCacheDirectory
        }
    }

    // Excluded in this context means:
    // Exclude from being deleted from the cache
    public func clearCache(exclude excludedSet: Set<CacheKey>) throws {
        let directoryContentURLs = cacheDirectoryContents
        let exludedFileNames = Set(excludedSet.map { $0.filenameWithoutExtension })
        for contentURL in directoryContentURLs {
            let lastPathComponent = contentURL.deletingPathExtension().lastPathComponent
            let isInExcludedSet = exludedFileNames.contains(lastPathComponent)
            if !isInExcludedSet {
                do {
                    try FileManager.default.removeItem(at: contentURL)
                } catch {
                    continue
                }
            }
        }
    }

    // Check if a resource is cached
    public func isResourceCached(_ key: CacheKey) -> Bool {
        guard let cacheDirectoryURL else { return false }
        return FileManager.default.fileExists(atPath: key.fileURL(cacheDirectoryURL).path)
    }

    // Retrieve cached file -> return nothing if it doesn't exist.
    public func getCachedFile(for key: CacheKey) -> URL? {
        guard isResourceCached(key), let cacheDirectoryURL else { return nil }
        return key.fileURL(cacheDirectoryURL)
    }

    public func downloadAndSetCacheResource(for key: CacheKey, from sourceURL: URL) async throws -> URL {
        guard let cacheDirectoryURL else {
            throw ShareAssetResourceError.noCacheRootDirectorySet
        }
        let fileURL = key.fileURL(cacheDirectoryURL)
        do {
            try clearCache(for: key)
            let (data, response) = try await URLSession.shared.data(for: .init(url: sourceURL))
            if let http = response as? HTTPURLResponse, !(200 ... 299).contains(http.statusCode) {
                throw ShareAssetResourceError.couldNotDownloadItem
            }
            try data.write(to: fileURL, options: .atomic)
            return fileURL
        } catch {
            print("ShareAsset: Error writing file \(key.resourceTag) to cache \(error)")
            throw ShareAssetResourceError.couldNotCreateSourceforResourceCache
        }
    }

    public func setCacheResource(for key: CacheKey, from sourceURL: URL) throws {
        guard let cacheDirectoryURL else {
            throw ShareAssetResourceError.noCacheRootDirectorySet
        }
        let fileURL = key.fileURL(cacheDirectoryURL)
        do {
            try clearCache(for: key)
            try FileManager.default.copyItem(at: sourceURL, to: fileURL)
        } catch {
            print("ShareAsset: Error writing file \(key.resourceTag) to cache \(error)")
            throw ShareAssetResourceError.couldNotCreateSourceforResourceCache
        }
    }

    public func clearCache(for key: CacheKey) throws {
        guard let cacheDirectoryURL else {
            throw ShareAssetResourceError.noCacheRootDirectorySet
        }
        let fileURL = key.fileURL(cacheDirectoryURL)
        if FileManager.default.fileExists(atPath: fileURL.path) {
            do {
                try FileManager.default.removeItem(at: fileURL)
            } catch {
                print("ShareAsset: Error clearing cache: \(error)")
                throw ShareAssetResourceError.couldNotClearCacheItem
            }
        }
    }
}

private extension ShareAssetResourceCache {
    enum Constants {
        static let cacheDirectoryComponent = "ShareAssetResourceCache"
    }

    var cacheDirectoryURL: URL? {
        return rootDirectory?.appendingPathComponent(Constants.cacheDirectoryComponent)
    }

    var cacheDirectoryContents: [URL] {
        guard let cacheDirectoryURL else { return [] }
        let directoryContentURLs = (
            try? FileManager.default
                .contentsOfDirectory(
                    at: cacheDirectoryURL,
                    includingPropertiesForKeys: nil
                )
        ) ?? []
        return directoryContentURLs
    }
}
