import Foundation

struct DirEntry: Identifiable, Hashable {
    var id: String { relativePath }
    var name: String
    var relativePath: String
    var isDir: Bool
    var resource: CatalogEntry?
}

struct DirListing {
    var path: String
    var entries: [DirEntry]
}

enum FileLibraryError: LocalizedError {
    case pathEscapesLibrary
    case invalidName
    case notFound
    case alreadyExists
    case io(String)

    var errorDescription: String? {
        switch self {
        case .pathEscapesLibrary: return "路径超出 library 根目录"
        case .invalidName: return "名称无效"
        case .notFound: return "路径不存在"
        case .alreadyExists: return "目标已存在"
        case .io(let message): return message
        }
    }
}

enum FileLibrary {
    static var documentsURL: URL {
        FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    }

    static var libraryRootURL: URL {
        documentsURL.appendingPathComponent("library", isDirectory: true)
    }

    static func ensureLibraryRoot() throws {
        try FileManager.default.createDirectory(
            at: libraryRootURL,
            withIntermediateDirectories: true
        )
    }

    static func normalizeRelative(_ path: String) -> String {
        path
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .trimmingCharacters(in: CharacterSet(charactersIn: "/\\"))
            .replacingOccurrences(of: "\\", with: "/")
    }

    /// Rejects `..` and absolute escapes.
    static func assertSafeRelative(_ relative: String) throws -> String {
        let normalized = normalizeRelative(relative)
        if normalized.isEmpty { return "" }
        let parts = normalized.split(separator: "/")
        if parts.contains("..") || parts.contains(".") {
            // allow "." only as empty; reject ".."
            if parts.contains("..") {
                throw FileLibraryError.pathEscapesLibrary
            }
        }
        if normalized.hasPrefix("/") {
            throw FileLibraryError.pathEscapesLibrary
        }
        return normalized
    }

    static func resolveURL(relative: String) throws -> URL {
        let rel = try assertSafeRelative(relative)
        let root = libraryRootURL.standardizedFileURL
        let url = rel.isEmpty ? root : root.appendingPathComponent(rel)
        let standardized = url.standardizedFileURL
        let rootPath = root.path
        let candidatePath = standardized.path
        if candidatePath != rootPath && !candidatePath.hasPrefix(rootPath + "/") {
            throw FileLibraryError.pathEscapesLibrary
        }
        return standardized
    }

    static func relativePath(for url: URL) throws -> String {
        let root = libraryRootURL.standardizedFileURL
        let standardized = url.standardizedFileURL
        let rootPath = root.path
        let path = standardized.path
        guard path == rootPath || path.hasPrefix(rootPath + "/") else {
            throw FileLibraryError.pathEscapesLibrary
        }
        if path == rootPath { return "" }
        return String(path.dropFirst(rootPath.count + 1))
    }

    static func uniqueDestination(in directory: URL, fileName: String) -> URL {
        let candidate = directory.appendingPathComponent(fileName)
        if !FileManager.default.fileExists(atPath: candidate.path) {
            return candidate
        }
        let name = (fileName as NSString).deletingPathExtension
        let ext = (fileName as NSString).pathExtension
        let suffix = ext.isEmpty ? "" : ".\(ext)"
        for i in 1..<10_000 {
            let next = directory.appendingPathComponent("\(name)_\(i)\(suffix)")
            if !FileManager.default.fileExists(atPath: next.path) {
                return next
            }
        }
        return directory.appendingPathComponent("\(name)_\(UUID().uuidString)\(suffix)")
    }

    static func listDirectory(relative: String) throws -> DirListing {
        try ensureLibraryRoot()
        let rel = try assertSafeRelative(relative)
        let url = try resolveURL(relative: rel)
        guard FileManager.default.fileExists(atPath: url.path) else {
            throw FileLibraryError.notFound
        }
        let contents = try FileManager.default.contentsOfDirectory(
            at: url,
            includingPropertiesForKeys: [.isDirectoryKey, .nameKey],
            options: [.skipsHiddenFiles]
        )
        var entries: [DirEntry] = []
        for item in contents.sorted(by: { $0.lastPathComponent.localizedStandardCompare($1.lastPathComponent) == .orderedAscending }) {
            let values = try item.resourceValues(forKeys: [.isDirectoryKey])
            let isDir = values.isDirectory ?? false
            let itemRel = try relativePath(for: item)
            entries.append(
                DirEntry(
                    name: item.lastPathComponent,
                    relativePath: itemRel,
                    isDir: isDir,
                    resource: nil
                )
            )
        }
        return DirListing(path: rel, entries: entries)
    }

    static func createFolder(parentDir: String, name: String) throws -> String {
        let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty, !trimmed.contains("/"), trimmed != "." && trimmed != ".." else {
            throw FileLibraryError.invalidName
        }
        let parent = try assertSafeRelative(parentDir)
        let destRel = parent.isEmpty ? trimmed : "\(parent)/\(trimmed)"
        let dest = try resolveURL(relative: destRel)
        if FileManager.default.fileExists(atPath: dest.path) {
            throw FileLibraryError.alreadyExists
        }
        try FileManager.default.createDirectory(at: dest, withIntermediateDirectories: true)
        return destRel
    }

    static func rename(relativePath: String, newName: String) throws -> String {
        let trimmed = newName.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty, !trimmed.contains("/"), trimmed != "." && trimmed != ".." else {
            throw FileLibraryError.invalidName
        }
        let srcRel = try assertSafeRelative(relativePath)
        guard !srcRel.isEmpty else { throw FileLibraryError.invalidName }
        let src = try resolveURL(relative: srcRel)
        guard FileManager.default.fileExists(atPath: src.path) else {
            throw FileLibraryError.notFound
        }
        let parent = (srcRel as NSString).deletingLastPathComponent
        let destRel = parent.isEmpty || parent == "." ? trimmed : "\(parent)/\(trimmed)"
        let dest = try resolveURL(relative: destRel)
        if FileManager.default.fileExists(atPath: dest.path) {
            throw FileLibraryError.alreadyExists
        }
        try FileManager.default.moveItem(at: src, to: dest)
        return destRel
    }

    static func move(relativePath: String, targetDir: String) throws -> String {
        let srcRel = try assertSafeRelative(relativePath)
        let destDir = try assertSafeRelative(targetDir)
        guard !srcRel.isEmpty else { throw FileLibraryError.invalidName }
        let src = try resolveURL(relative: srcRel)
        guard FileManager.default.fileExists(atPath: src.path) else {
            throw FileLibraryError.notFound
        }
        let base = (srcRel as NSString).lastPathComponent
        let destRel = destDir.isEmpty ? base : "\(destDir)/\(base)"
        // Prevent moving a folder into itself
        if destRel == srcRel || destRel.hasPrefix(srcRel + "/") {
            throw FileLibraryError.pathEscapesLibrary
        }
        let destParent = try resolveURL(relative: destDir)
        try FileManager.default.createDirectory(at: destParent, withIntermediateDirectories: true)
        let dest = try resolveURL(relative: destRel)
        if FileManager.default.fileExists(atPath: dest.path) {
            throw FileLibraryError.alreadyExists
        }
        try FileManager.default.moveItem(at: src, to: dest)
        return destRel
    }

    static func delete(relativePath: String) throws {
        let rel = try assertSafeRelative(relativePath)
        guard !rel.isEmpty else { throw FileLibraryError.invalidName }
        let url = try resolveURL(relative: rel)
        guard FileManager.default.fileExists(atPath: url.path) else {
            throw FileLibraryError.notFound
        }
        try FileManager.default.removeItem(at: url)
    }

    static func storageFootprintBytes() -> Int64 {
        guard let enumerator = FileManager.default.enumerator(
            at: libraryRootURL,
            includingPropertiesForKeys: [.fileSizeKey, .isRegularFileKey]
        ) else { return 0 }
        var total: Int64 = 0
        for case let url as URL in enumerator {
            guard let values = try? url.resourceValues(forKeys: [.isRegularFileKey, .fileSizeKey]),
                  values.isRegularFile == true,
                  let size = values.fileSize else { continue }
            total += Int64(size)
        }
        return total
    }

    static func clearLibraryContents() throws {
        try ensureLibraryRoot()
        let contents = try FileManager.default.contentsOfDirectory(
            at: libraryRootURL,
            includingPropertiesForKeys: nil
        )
        for item in contents {
            try FileManager.default.removeItem(at: item)
        }
    }
}
