//
//  TravelAlbumOTGPhotoStore.swift
//  suixinkan
//

import Foundation

/// 生成并规范化旅拍照片的客户端唯一标识。
enum TravelAlbumClientPhotoID {
    /// 生成小写、带连字符的 UUID v4 字符串。
    static func make() -> String {
        UUID().uuidString.lowercased()
    }

    /// 规范化服务端或本地保存的客户端照片 ID。
    static func normalize(_ value: String?) -> String? {
        guard let normalized = value?
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .lowercased(),
              !normalized.isEmpty else {
            return nil
        }
        return normalized
    }
}

/// OTG 传输照片上传状态，对齐 Android `WiredTransferUploadStatus`。
enum TravelAlbumOTGUploadStatus: String, Codable, Equatable, Sendable {
    case pending = "PENDING"
    case transferring = "TRANSFERRING"
    case uploading = "UPLOADING"
    case uploaded = "UPLOADED"
    case failed = "FAILED"
}

/// OTG 照片格式上传偏好。
enum TravelAlbumOTGPhotoFormatOption: String, CaseIterable, Equatable, Sendable {
    case jpg = "JPG"
    case raw = "RAW"
    case jpegRaw = "JPEG+RAW"
}

/// OTG 本地照片记录，按账号、景区、门店与服务端相册 ID 隔离保存。
struct TravelAlbumOTGPhotoRecord: Codable, Hashable, Sendable {
    let id: String
    let sourceId: String
    var clientPhotoId: String
    let fileName: String
    var localPath: String
    var thumbnailPath: String
    let capturedAt: String
    let fileSizeBytes: Int64
    var status: TravelAlbumOTGUploadStatus
    var progress: Int
    var errorMessage: String?
    let albumId: Int
    let userId: String
    var remoteUrl: String
    var updatedAt: Int64

    /// 创建 OTG 本地照片记录。
    init(
        id: String,
        sourceId: String? = nil,
        clientPhotoId: String = TravelAlbumClientPhotoID.make(),
        fileName: String,
        localPath: String,
        thumbnailPath: String = "",
        capturedAt: String,
        fileSizeBytes: Int64,
        status: TravelAlbumOTGUploadStatus,
        progress: Int = 0,
        errorMessage: String? = nil,
        albumId: Int,
        userId: String,
        remoteUrl: String = "",
        updatedAt: Int64 = Int64(Date().timeIntervalSince1970 * 1000)
    ) {
        self.id = id
        self.sourceId = sourceId ?? id
        self.clientPhotoId = TravelAlbumClientPhotoID.normalize(clientPhotoId) ?? TravelAlbumClientPhotoID.make()
        self.fileName = fileName
        self.localPath = localPath
        self.thumbnailPath = thumbnailPath
        self.capturedAt = capturedAt
        self.fileSizeBytes = fileSizeBytes
        self.status = status
        self.progress = progress
        self.errorMessage = errorMessage
        self.albumId = albumId
        self.userId = userId
        self.remoteUrl = remoteUrl
        self.updatedAt = updatedAt
    }

    private enum CodingKeys: String, CodingKey {
        case id, sourceId, clientPhotoId, fileName, localPath, thumbnailPath, capturedAt
        case fileSizeBytes, status, progress, errorMessage, albumId, userId, remoteUrl, updatedAt
    }

    /// 解码本地索引；旧版本缺少 `clientPhotoId` 时先保留为空，由 Store 一次性迁移并回写。
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        sourceId = try container.decodeIfPresent(String.self, forKey: .sourceId) ?? id
        clientPhotoId = try container.decodeIfPresent(String.self, forKey: .clientPhotoId) ?? ""
        fileName = try container.decode(String.self, forKey: .fileName)
        localPath = try container.decode(String.self, forKey: .localPath)
        thumbnailPath = try container.decodeIfPresent(String.self, forKey: .thumbnailPath) ?? ""
        capturedAt = try container.decode(String.self, forKey: .capturedAt)
        fileSizeBytes = try container.decodeIfPresent(Int64.self, forKey: .fileSizeBytes) ?? 0
        status = try container.decode(TravelAlbumOTGUploadStatus.self, forKey: .status)
        progress = try container.decodeIfPresent(Int.self, forKey: .progress) ?? 0
        errorMessage = try container.decodeIfPresent(String.self, forKey: .errorMessage)
        albumId = try container.decode(Int.self, forKey: .albumId)
        userId = try container.decode(String.self, forKey: .userId)
        remoteUrl = try container.decodeIfPresent(String.self, forKey: .remoteUrl) ?? ""
        updatedAt = try container.decodeIfPresent(Int64.self, forKey: .updatedAt) ?? 0
    }

    /// 把中断中的传输恢复为待上传，避免重进页面卡在上传中。
    func normalizedAfterInterruptedTransfer() -> TravelAlbumOTGPhotoRecord {
        guard status == .transferring || status == .uploading else { return self }
        var copy = self
        copy.status = .pending
        copy.progress = 0
        copy.errorMessage = nil
        copy.updatedAt = Int64(Date().timeIntervalSince1970 * 1000)
        return copy
    }
}

/// 相册内照片的持久化读写接口，供 OTG 边拍边传服务写入。
protocol PhotoRepositoryProtocol {
    /// 读取指定服务端相册 ID 的本地照片记录。
    func fetchPhotos(albumID: Int) throws -> [TravelAlbumOTGPhotoRecord]

    /// 添加一张已下载到本地的照片。
    @discardableResult
    func addPhoto(albumID: Int, localPath: String, createdAt: Date) throws -> TravelAlbumOTGPhotoRecord

    /// 删除指定照片及本地文件。
    func deletePhoto(photoID: String, albumID: Int) throws
}

/// OTG 本地照片仓库，使用 JSON 索引和隔离目录保存照片。
final class TravelAlbumOTGPhotoRepository: PhotoRepositoryProtocol {
    private let storage: TravelAlbumOTGPhotoStore

    /// 初始化 OTG 本地照片仓库。
    init(storage: TravelAlbumOTGPhotoStore = TravelAlbumOTGPhotoStore()) {
        self.storage = storage
    }

    /// 读取指定服务端相册 ID 的本地照片记录。
    func fetchPhotos(albumID: Int) throws -> [TravelAlbumOTGPhotoRecord] {
        storage.load(albumId: albumID)
    }

    /// 添加一张已下载到本地的照片。
    @discardableResult
    func addPhoto(albumID: Int, localPath: String, createdAt: Date) throws -> TravelAlbumOTGPhotoRecord {
        let fileURL = URL(fileURLWithPath: localPath)
        let attributes = try? FileManager.default.attributesOfItem(atPath: localPath)
        let size = (attributes?[.size] as? NSNumber)?.int64Value ?? 0
        let userId = storage.context.userId
        guard !userId.isEmpty else {
            throw CameraError.connectionFailed("用户信息缺失，无法保存照片")
        }

        let record = TravelAlbumOTGPhotoRecord(
            id: TravelAlbumOTGPhotoStore.photoId(for: fileURL.lastPathComponent, createdAt: createdAt),
            fileName: fileURL.lastPathComponent,
            localPath: storage.relativePath(for: fileURL, albumId: albumID),
            capturedAt: TravelAlbumOTGPhotoStore.transferTimeText(createdAt),
            fileSizeBytes: size,
            status: .pending,
            albumId: albumID,
            userId: userId
        )
        storage.upsert(record, albumId: albumID)
        return record
    }

    /// 删除指定照片及本地文件。
    func deletePhoto(photoID: String, albumID: Int) throws {
        storage.remove(albumId: albumID, photoId: photoID)
    }
}

/// OTG 本地存储上下文，决定照片和索引的隔离目录。
struct TravelAlbumOTGStorageContext: Equatable, Sendable {
    let accountCachePrefix: String
    let userId: String
    let scenicId: Int
    let storeId: Int

    /// 使用当前登录账号创建存储上下文。
    static var current: TravelAlbumOTGStorageContext {
        TravelAlbumOTGStorageContext(
            accountCachePrefix: AppStore.shared.session.accountCachePrefix,
            userId: AppStore.shared.session.userId,
            scenicId: AppStore.shared.session.currentScenicId,
            storeId: AppStore.shared.session.currentStoreId
        )
    }
}

/// 旅拍相册 OTG 本地照片存储，负责路径隔离、索引读写与文件清理。
final class TravelAlbumOTGPhotoStore {
    let context: TravelAlbumOTGStorageContext
    private let fileManager: FileManager
    private let applicationSupportDirectory: URL
    private let cachesDirectory: URL

    /// 初始化旅拍相册 OTG 存储。
    init(
        context: TravelAlbumOTGStorageContext = .current,
        fileManager: FileManager = .default,
        applicationSupportDirectory: URL? = nil,
        cachesDirectory: URL? = nil
    ) {
        self.context = context
        self.fileManager = fileManager
        self.applicationSupportDirectory = applicationSupportDirectory
            ?? fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
        self.cachesDirectory = cachesDirectory
            ?? fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
    }

    /// 读取相册照片记录。
    func load(albumId: Int) -> [TravelAlbumOTGPhotoRecord] {
        guard albumId > 0, !context.userId.isEmpty else { return [] }
        guard let data = try? Data(contentsOf: indexURL(albumId: albumId)),
              let decoded = try? JSONDecoder().decode([TravelAlbumOTGPhotoRecord].self, from: data) else {
            return []
        }
        let scoped = decoded.filter { $0.albumId == albumId && $0.userId == context.userId }
        let migrated = scoped.map { record -> TravelAlbumOTGPhotoRecord in
            var copy = record
            copy.clientPhotoId = TravelAlbumClientPhotoID.normalize(record.clientPhotoId)
                ?? TravelAlbumClientPhotoID.make()
            return copy.normalizedAfterInterruptedTransfer()
        }
        if migrated.map(\.clientPhotoId) != scoped.map(\.clientPhotoId) {
            save(migrated, albumId: albumId)
        }
        return migrated
            .filter { isDisplayable($0, albumId: albumId) }
            .sorted { $0.capturedAt > $1.capturedAt }
    }

    /// 保存相册照片记录。
    func save(_ records: [TravelAlbumOTGPhotoRecord], albumId: Int) {
        guard albumId > 0, !context.userId.isEmpty else { return }
        let normalized = records
            .filter { $0.albumId == albumId && $0.userId == context.userId }
            .map { recordByPersistingRelativePaths($0, albumId: albumId) }
            .sorted { $0.capturedAt > $1.capturedAt }
        do {
            try fileManager.createDirectory(at: albumRoot(albumId: albumId), withIntermediateDirectories: true)
            let data = try JSONEncoder().encode(normalized)
            try data.write(to: indexURL(albumId: albumId), options: .atomic)
        } catch {
            OTGLog.error(.connection, "save OTG photo records failed: \(error.localizedDescription)")
        }
    }

    /// 插入或更新单条照片记录。
    func upsert(_ record: TravelAlbumOTGPhotoRecord, albumId: Int) {
        var records = load(albumId: albumId).filter { $0.id != record.id }
        records.append(record)
        save(records, albumId: albumId)
    }

    /// 删除单张照片记录并清理本地文件。
    func remove(albumId: Int, photoId: String) {
        guard albumId > 0, !photoId.isEmpty else { return }
        let records = load(albumId: albumId)
        records.filter { $0.id == photoId }.forEach(deleteFiles)
        save(records.filter { $0.id != photoId }, albumId: albumId)
    }

    /// 清理整个相册的 OTG 本地缓存。
    func clearAlbum(albumId: Int) {
        guard albumId > 0 else { return }
        try? fileManager.removeItem(at: albumRoot(albumId: albumId))
        try? fileManager.removeItem(at: previewRoot(albumId: albumId))
    }

    /// 原片目录。
    func originalsDirectory(albumId: Int) throws -> URL {
        let url = albumRoot(albumId: albumId).appendingPathComponent("originals", isDirectory: true)
        try fileManager.createDirectory(at: url, withIntermediateDirectories: true)
        try excludeFromBackup(url)
        return url
    }

    /// 预览目录。
    func previewsDirectory(albumId: Int) throws -> URL {
        let url = previewRoot(albumId: albumId)
        try fileManager.createDirectory(at: url, withIntermediateDirectories: true)
        return url
    }

    /// 在隔离原片目录下分配不冲突文件 URL。
    func uniqueOriginalFileURL(filename: String, albumId: Int) throws -> URL {
        let directory = try originalsDirectory(albumId: albumId)
        let sanitized = PTPHelper.sanitizeFilename(filename)
        let baseURL = directory.appendingPathComponent(sanitized)
        if !fileManager.fileExists(atPath: baseURL.path) {
            return baseURL
        }

        let stem = (sanitized as NSString).deletingPathExtension
        let ext = (sanitized as NSString).pathExtension
        var counter = 1
        while counter < 10_000 {
            let suffix = ext.isEmpty ? "\(stem)_\(counter)" : "\(stem)_\(counter).\(ext)"
            let candidate = directory.appendingPathComponent(suffix)
            if !fileManager.fileExists(atPath: candidate.path) {
                return candidate
            }
            counter += 1
        }
        throw CameraError.connectionFailed("无法分配唯一文件名")
    }

    /// 写入原片数据，返回隔离目录中的文件 URL。
    func writeImage(_ data: Data, filename: String, albumId: Int) throws -> URL {
        let url = try uniqueOriginalFileURL(filename: filename, albumId: albumId)
        try data.write(to: url, options: .atomic)
        return url
    }

    /// 返回写入索引用的相对路径。
    func relativePath(for url: URL, albumId: Int) -> String {
        let path = url.standardizedFileURL.path
        let originalRoot = albumRoot(albumId: albumId)
            .appendingPathComponent("originals", isDirectory: true)
            .standardizedFileURL
            .path
        if let relative = pathComponent(path, relativeTo: originalRoot) {
            return "originals/\(relative)"
        }

        let previewPath = previewRoot(albumId: albumId).standardizedFileURL.path
        if let relative = pathComponent(path, relativeTo: previewPath) {
            return "previews/\(relative)"
        }

        return url.lastPathComponent
    }

    /// 把索引中的相对路径解析到当前沙盒绝对 URL。
    func absoluteURL(for relativePath: String, albumId: Int) -> URL? {
        let trimmed = relativePath.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty, !trimmed.hasPrefix("/") else { return nil }
        if trimmed.hasPrefix("originals/") {
            return albumRoot(albumId: albumId).appendingPathComponent(trimmed)
        }
        if trimmed.hasPrefix("previews/") {
            return previewRoot(albumId: albumId)
                .deletingLastPathComponent()
                .appendingPathComponent(trimmed)
        }
        return albumRoot(albumId: albumId).appendingPathComponent(trimmed)
    }

    /// 判断索引相对路径对应的文件是否存在。
    func fileExists(relativePath: String, albumId: Int) -> Bool {
        guard let url = absoluteURL(for: relativePath, albumId: albumId) else { return false }
        return fileManager.fileExists(atPath: url.path)
    }

    /// 返回可直接用于文件读取的记录副本。
    func recordWithResolvedFilePaths(_ record: TravelAlbumOTGPhotoRecord, albumId: Int) -> TravelAlbumOTGPhotoRecord {
        var resolved = record
        if let localURL = absoluteURL(for: record.localPath, albumId: albumId) {
            resolved.localPath = localURL.path
        }
        if let thumbnailURL = absoluteURL(for: record.thumbnailPath, albumId: albumId) {
            resolved.thumbnailPath = thumbnailURL.path
        }
        return resolved
    }

    /// 生成稳定照片 ID。
    static func photoId(for fileName: String, createdAt: Date) -> String {
        let timestamp = Int64(createdAt.timeIntervalSince1970)
        return "\(fileName.uppercased())_\(timestamp)"
    }

    /// Android 同款时间格式。
    static func transferTimeText(_ date: Date) -> String {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "zh_CN")
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        return formatter.string(from: date)
    }

    /// 格式化文件大小。
    static func fileSizeText(_ bytes: Int64) -> String {
        guard bytes > 0 else { return "--" }
        let mb = Double(bytes) / 1024.0 / 1024.0
        if mb >= 1024 {
            return String(format: "%.2f GB", locale: Locale(identifier: "en_US_POSIX"), mb / 1024.0)
        }
        return String(format: "%.2f MB", locale: Locale(identifier: "en_US_POSIX"), mb)
    }

    private func albumRoot(albumId: Int) -> URL {
        applicationSupportDirectory
            .appendingPathComponent("TravelAlbumOTG", isDirectory: true)
            .appendingPathComponent(safePathComponent(context.accountCachePrefix), isDirectory: true)
            .appendingPathComponent("scenic_\(context.scenicId)", isDirectory: true)
            .appendingPathComponent("store_\(context.storeId)", isDirectory: true)
            .appendingPathComponent("album_\(albumId)", isDirectory: true)
    }

    private func previewRoot(albumId: Int) -> URL {
        cachesDirectory
            .appendingPathComponent("TravelAlbumOTG", isDirectory: true)
            .appendingPathComponent(safePathComponent(context.accountCachePrefix), isDirectory: true)
            .appendingPathComponent("scenic_\(context.scenicId)", isDirectory: true)
            .appendingPathComponent("store_\(context.storeId)", isDirectory: true)
            .appendingPathComponent("album_\(albumId)", isDirectory: true)
            .appendingPathComponent("previews", isDirectory: true)
    }

    private func indexURL(albumId: Int) -> URL {
        albumRoot(albumId: albumId).appendingPathComponent("records.json")
    }

    private func deleteFiles(_ record: TravelAlbumOTGPhotoRecord) {
        [record.localPath, record.thumbnailPath].forEach { path in
            guard !path.isEmpty else { return }
            guard let url = absoluteURL(for: path, albumId: record.albumId) else { return }
            try? fileManager.removeItem(at: url)
        }
    }

    private func isDisplayable(_ record: TravelAlbumOTGPhotoRecord, albumId: Int) -> Bool {
        if record.status == .uploaded { return true }
        if !record.remoteUrl.isEmpty { return true }
        if fileExists(relativePath: record.localPath, albumId: albumId) { return true }
        if fileExists(relativePath: record.thumbnailPath, albumId: albumId) { return true }
        return false
    }

    private func recordByPersistingRelativePaths(
        _ record: TravelAlbumOTGPhotoRecord,
        albumId: Int
    ) -> TravelAlbumOTGPhotoRecord {
        var normalized = record
        if !record.localPath.isEmpty {
            normalized.localPath = record.localPath.hasPrefix("/")
                ? relativePath(for: URL(fileURLWithPath: record.localPath), albumId: albumId)
                : record.localPath
        }
        if !record.thumbnailPath.isEmpty {
            normalized.thumbnailPath = record.thumbnailPath.hasPrefix("/")
                ? relativePath(for: URL(fileURLWithPath: record.thumbnailPath), albumId: albumId)
                : record.thumbnailPath
        }
        return normalized
    }

    private func pathComponent(_ path: String, relativeTo root: String) -> String? {
        let prefix = root.hasSuffix("/") ? root : "\(root)/"
        guard path.hasPrefix(prefix) else { return nil }
        return String(path.dropFirst(prefix.count))
    }

    private func excludeFromBackup(_ url: URL) throws {
        var resourceValues = URLResourceValues()
        resourceValues.isExcludedFromBackup = true
        var mutableURL = url
        try mutableURL.setResourceValues(resourceValues)
    }

    private func safePathComponent(_ value: String) -> String {
        let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return "guest" }
        let unsafe = CharacterSet(charactersIn: "/\\:?%*|\"<>")
        return trimmed.unicodeScalars.reduce(into: "") { result, scalar in
            result += unsafe.contains(scalar) ? "_" : String(scalar)
        }
    }
}

/// 按服务端客户端照片 ID 快照回写本地持久状态。
enum TravelAlbumOTGServerStatusPolicy {
    /// 只处理请求发起时已存在且期间未变化的非活动记录。
    static func reconcile(
        records: [TravelAlbumOTGPhotoRecord],
        baselineRecordsById: [String: TravelAlbumOTGPhotoRecord],
        activePhotoIds: Set<String>,
        serverClientPhotoIds: [String],
        now: Int64
    ) -> [TravelAlbumOTGPhotoRecord] {
        let serverIds = Set(serverClientPhotoIds.compactMap(TravelAlbumClientPhotoID.normalize))
        return records.map { record in
            guard baselineRecordsById[record.id] == record,
                  !activePhotoIds.contains(record.id) else {
                return record
            }
            var copy = record
            if let clientPhotoId = TravelAlbumClientPhotoID.normalize(record.clientPhotoId),
               serverIds.contains(clientPhotoId) {
                copy.status = .uploaded
                copy.progress = 100
                copy.errorMessage = nil
                copy.updatedAt = now
            } else if record.status == .uploaded {
                copy.status = .pending
                copy.progress = 0
                copy.errorMessage = nil
                copy.updatedAt = now
            }
            return copy
        }
    }
}

/// 兼容从 `otg_swift` 同步来的边拍边传写入调用。
enum AlbumPhotoStorage {
    /// 写入原片数据到当前账号与相册隔离目录。
    static func writeImage(_ data: Data, filename: String, albumID: Int) throws -> URL {
        try TravelAlbumOTGPhotoStore().writeImage(data, filename: filename, albumId: albumID)
    }
}
