import Foundation

enum CourseLibrary {
    static let coursesFolderName = "courses"
    static let schemaMigrationKey = "childDeckSchemaVersion"
    static let currentSchemaVersion = 1

    static var documentsURL: URL {
        FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    }

    static var coursesRootURL: URL {
        documentsURL.appendingPathComponent(coursesFolderName, isDirectory: true)
    }

    static func ensureCoursesRoot() throws {
        try FileManager.default.createDirectory(at: coursesRootURL, withIntermediateDirectories: true)
    }

    static func courseDirectory(id: String) throws -> URL {
        let safe = sanitizeCourseId(id)
        let url = coursesRootURL.appendingPathComponent(safe, isDirectory: true)
        guard url.standardizedFileURL.path.hasPrefix(coursesRootURL.standardizedFileURL.path) else {
            throw CoursePackError.pathEscape
        }
        return url
    }

    static func manifestURL(courseId: String) throws -> URL {
        try courseDirectory(id: courseId).appendingPathComponent("manifest.json")
    }

    static func sanitizeCourseId(_ id: String) -> String {
        id.replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "..", with: "_")
    }

    static func loadManifest(courseId: String) throws -> CourseManifest {
        let url = try manifestURL(courseId: courseId)
        let data = try Data(contentsOf: url)
        let manifest = try JSONDecoder().decode(CourseManifest.self, from: data)
        try manifest.validate()
        return manifest
    }

    static func fileURL(courseId: String, resourcePath: String) throws -> URL {
        let root = try courseDirectory(id: courseId)
        let cleaned = resourcePath.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
        guard !cleaned.contains("..") else { throw CoursePackError.pathEscape }
        let url = root.appendingPathComponent(cleaned)
        guard url.standardizedFileURL.path.hasPrefix(root.standardizedFileURL.path) else {
            throw CoursePackError.pathEscape
        }
        return url
    }

    static func resolveMediaURL(courseId: String, resourceId: String?) throws -> URL? {
        guard let resourceId else { return nil }
        let manifest = try loadManifest(courseId: courseId)
        guard let resource = manifest.resource(id: resourceId) else { return nil }
        let url = try fileURL(courseId: courseId, resourcePath: resource.path)
        guard FileManager.default.fileExists(atPath: url.path) else {
            throw CoursePackError.missingMedia(resource.path)
        }
        return url
    }

    static func storageByteCount() -> Int64 {
        guard FileManager.default.fileExists(atPath: coursesRootURL.path),
              let enumerator = FileManager.default.enumerator(
                at: coursesRootURL,
                includingPropertiesForKeys: [.fileSizeKey],
                options: [.skipsHiddenFiles]
              ) else { return 0 }
        var total: Int64 = 0
        for case let url as URL in enumerator {
            let size = (try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize).map(Int64.init) ?? 0
            total += size
        }
        return total
    }

    static func installUnpackedDirectory(at source: URL) throws -> CourseManifest {
        try ensureCoursesRoot()
        let manifestFile = source.appendingPathComponent("manifest.json")
        let data = try Data(contentsOf: manifestFile)
        let manifest = try JSONDecoder().decode(CourseManifest.self, from: data)
        try manifest.validate()

        for resource in manifest.resources {
            let media = source.appendingPathComponent(resource.path)
            guard FileManager.default.fileExists(atPath: media.path) else {
                throw CoursePackError.missingMedia(resource.path)
            }
        }

        let dest = try courseDirectory(id: manifest.id)
        if FileManager.default.fileExists(atPath: dest.path) {
            try FileManager.default.removeItem(at: dest)
        }
        try FileManager.default.copyItem(at: source, to: dest)
        return try loadManifest(courseId: manifest.id)
    }

    static func removeCourse(id: String) throws {
        let dest = try courseDirectory(id: id)
        if FileManager.default.fileExists(atPath: dest.path) {
            try FileManager.default.removeItem(at: dest)
        }
    }

    static func unzipPack(zipURL: URL) throws -> URL {
        let temp = FileManager.default.temporaryDirectory
            .appendingPathComponent("studydeck-pack-\(UUID().uuidString)", isDirectory: true)
        try FileManager.default.createDirectory(at: temp, withIntermediateDirectories: true)
        do {
            try ZipUtility.unzip(fileAt: zipURL, to: temp)
        } catch {
            try? FileManager.default.removeItem(at: temp)
            throw error
        }
        return normalizeUnzipRoot(temp)
    }

    private static func normalizeUnzipRoot(_ temp: URL) -> URL {
        let contents = (try? FileManager.default.contentsOfDirectory(
            at: temp,
            includingPropertiesForKeys: [.isDirectoryKey],
            options: [.skipsHiddenFiles]
        )) ?? []
        if contents.count == 1,
           contents[0].hasDirectoryPath,
           FileManager.default.fileExists(atPath: contents[0].appendingPathComponent("manifest.json").path) {
            return contents[0]
        }
        return temp
    }
}
