import AVFoundation
import Photos
import PhotosUI
import SwiftUI
import UniformTypeIdentifiers

/// The source library: clip references + granted folders, persisted as JSON.
/// Video bytes are never copied — a clip is a bookmark or a PHAsset ID plus a
/// fingerprint for relinking.
@MainActor
final class SourceLibrary: ObservableObject {
    @Published private(set) var clips: [SourceClip] = []
    @Published private(set) var folders: [GrantedFolder] = []
    @Published private(set) var availability: [UUID: ClipAvailability] = [:]
    @Published var lastError: String?

    private let storeURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        .appendingPathComponent("SourceLibrary.json")

    init() {
        load()
        Task { await refreshAvailability() }
    }

    // MARK: - Persistence

    private struct Snapshot: Codable {
        var clips: [SourceClip]
        var folders: [GrantedFolder]
    }

    private func load() {
        guard let data = try? Data(contentsOf: storeURL),
              let snapshot = try? JSONDecoder().decode(Snapshot.self, from: data) else { return }
        clips = snapshot.clips
        folders = snapshot.folders
    }

    private func save() {
        guard let data = try? JSONEncoder().encode(Snapshot(clips: clips, folders: folders)) else { return }
        try? data.write(to: storeURL, options: .atomic)
    }

    // MARK: - Import: files (external drive / Files app)

    func importFiles(at urls: [URL]) async {
        for url in urls {
            guard url.startAccessingSecurityScopedResource() else {
                lastError = "Couldn't access \(url.lastPathComponent)."
                continue
            }
            defer { url.stopAccessingSecurityScopedResource() }
            do {
                let bookmark = try BookmarkService.createBookmark(for: url)
                let fingerprint = try await FingerprintService.fingerprint(forFileAt: url)
                let clip = SourceClip(
                    id: UUID(),
                    displayName: url.lastPathComponent,
                    ref: SourceRef(kind: .file, bookmarkData: bookmark, photosAssetID: nil),
                    fingerprint: fingerprint,
                    addedAt: Date(),
                    previousRef: nil,
                    debugBroken: nil
                )
                if let thumbnail = await ThumbnailService.generate(from: AVURLAsset(url: url)) {
                    ThumbnailService.save(thumbnail, for: clip.id)
                }
                clips.append(clip)
                availability[clip.id] = .available
            } catch {
                lastError = "Import failed for \(url.lastPathComponent): \(error.localizedDescription)"
            }
        }
        save()
    }

    // MARK: - Import: Photos library

    func importPhotosItems(_ items: [PhotosPickerItem]) async {
        guard !items.isEmpty else { return }
        guard await PhotosService.requestReadAccess() else {
            lastError = "Photo library access is required to reference clips without copying them."
            return
        }
        for item in items {
            guard let identifier = item.itemIdentifier,
                  let asset = PhotosService.fetchAsset(identifier: identifier) else {
                lastError = "Couldn't resolve a selected Photos item."
                continue
            }
            let fingerprint = await FingerprintService.fingerprint(for: asset)
            let resources = PHAssetResource.assetResources(for: asset)
            let name = resources.first(where: { $0.type == .video })?.originalFilename
                ?? resources.first?.originalFilename
                ?? "Photos clip"
            let clip = SourceClip(
                id: UUID(),
                displayName: name,
                ref: SourceRef(kind: .photos, bookmarkData: nil, photosAssetID: identifier),
                fingerprint: fingerprint,
                addedAt: Date(),
                previousRef: nil,
                debugBroken: nil
            )
            if let avAsset = await PhotosService.requestAVAsset(for: asset),
               let thumbnail = await ThumbnailService.generate(from: avAsset) {
                ThumbnailService.save(thumbnail, for: clip.id)
            }
            clips.append(clip)
            availability[clip.id] = .available
        }
        save()
    }

    // MARK: - Availability

    func refreshAvailability() async {
        for clip in clips {
            availability[clip.id] = check(clip)
        }
    }

    private func check(_ clip: SourceClip) -> ClipAvailability {
        if clip.isDebugBroken {
            return .missing(reason: "Simulated broken reference")
        }
        switch clip.ref.kind {
        case .file:
            guard let data = clip.ref.bookmarkData,
                  let resolved = BookmarkService.resolve(data) else {
                return .missing(reason: "Bookmark can't be resolved")
            }
            // The system flagged the bookmark stale but recovered the file —
            // persist the fresh bookmark so next launch resolves directly.
            if let refreshed = resolved.refreshedBookmark,
               let index = clips.firstIndex(where: { $0.id == clip.id }) {
                clips[index].ref.bookmarkData = refreshed
                save()
            }
            let accessing = resolved.url.startAccessingSecurityScopedResource()
            defer { if accessing { resolved.url.stopAccessingSecurityScopedResource() } }
            let reachable = (try? resolved.url.checkResourceIsReachable()) ?? false
            return reachable ? .available : .missing(reason: "File unreachable — drive disconnected or file deleted")
        case .photos:
            guard PhotosService.hasReadAccess else {
                return .missing(reason: "No Photos access")
            }
            guard let identifier = clip.ref.photosAssetID,
                  PhotosService.fetchAsset(identifier: identifier) != nil else {
                return .missing(reason: "Photos asset no longer exists")
            }
            return .available
        }
    }

    // MARK: - Playback

    private var activePlaybackURL: URL?

    /// For file clips this starts security-scoped access; call `endPlayback()`
    /// when the player is dismissed.
    func beginPlayback(for clip: SourceClip) async -> AVPlayerItem? {
        switch clip.ref.kind {
        case .file:
            guard let data = clip.ref.bookmarkData,
                  let resolved = BookmarkService.resolve(data) else { return nil }
            if resolved.url.startAccessingSecurityScopedResource() {
                activePlaybackURL = resolved.url
            }
            return AVPlayerItem(url: resolved.url)
        case .photos:
            guard let identifier = clip.ref.photosAssetID,
                  let asset = PhotosService.fetchAsset(identifier: identifier) else { return nil }
            return await PhotosService.requestPlayerItem(for: asset)
        }
    }

    func endPlayback() {
        activePlaybackURL?.stopAccessingSecurityScopedResource()
        activePlaybackURL = nil
    }

    // MARK: - Granted folders

    func addFolder(url: URL) {
        guard url.startAccessingSecurityScopedResource() else {
            lastError = "Couldn't access folder \(url.lastPathComponent)."
            return
        }
        defer { url.stopAccessingSecurityScopedResource() }
        do {
            let bookmark = try BookmarkService.createBookmark(for: url)
            folders.append(GrantedFolder(id: UUID(), bookmarkData: bookmark, displayName: url.lastPathComponent))
            save()
        } catch {
            lastError = "Couldn't save folder access: \(error.localizedDescription)"
        }
    }

    func removeFolder(_ folder: GrantedFolder) {
        folders.removeAll { $0.id == folder.id }
        save()
    }

    // MARK: - Relink

    /// Scans every granted folder for files fingerprint-matching the clip.
    /// No reprompt needed — the folder bookmarks carry the access grant.
    func scanForCandidates(for clip: SourceClip) async -> [RelinkCandidate] {
        var candidates: [RelinkCandidate] = []
        for folder in folders {
            guard let resolved = BookmarkService.resolve(folder.bookmarkData) else { continue }
            let folderURL = resolved.url
            let accessing = folderURL.startAccessingSecurityScopedResource()
            defer { if accessing { folderURL.stopAccessingSecurityScopedResource() } }
            for fileURL in collectVideoFiles(in: folderURL) {
                guard let fingerprint = try? await FingerprintService.fingerprint(forFileAt: fileURL) else { continue }
                let score = FingerprintService.matchScore(candidate: fingerprint, target: clip.fingerprint)
                guard score >= 0.5 else { continue }
                let thumbnail = await ThumbnailService.generate(from: AVURLAsset(url: fileURL))
                candidates.append(RelinkCandidate(
                    fileURL: fileURL,
                    folderID: folder.id,
                    fingerprint: fingerprint,
                    score: score,
                    thumbnail: thumbnail,
                    isDurationSufficient: fingerprint.duration + 0.1 >= clip.fingerprint.duration
                ))
            }
        }
        return candidates.sorted { $0.score > $1.score }
    }

    /// Synchronous walk of the folder — the enumerator can't be carried across
    /// suspension points, so gather the URLs before the async fingerprint pass.
    private func collectVideoFiles(in folderURL: URL) -> [URL] {
        guard let enumerator = FileManager.default.enumerator(
            at: folderURL,
            includingPropertiesForKeys: [.fileSizeKey, .creationDateKey, .contentTypeKey],
            options: [.skipsHiddenFiles]
        ) else { return [] }
        var urls: [URL] = []
        for case let fileURL as URL in enumerator where isVideoFile(fileURL) {
            urls.append(fileURL)
        }
        return urls
    }

    private func isVideoFile(_ url: URL) -> Bool {
        guard let type = try? url.resourceValues(forKeys: [.contentTypeKey]).contentType else {
            return ["mov", "mp4", "m4v"].contains(url.pathExtension.lowercased())
        }
        return type.conforms(to: .movie) || type.conforms(to: .video)
    }

    func relink(clipID: UUID, to candidate: RelinkCandidate) {
        guard let index = clips.firstIndex(where: { $0.id == clipID }) else { return }
        guard let folder = folders.first(where: { $0.id == candidate.folderID }),
              let resolvedFolder = BookmarkService.resolve(folder.bookmarkData) else {
            lastError = "Lost access to the folder containing the matched file."
            return
        }
        let accessing = resolvedFolder.url.startAccessingSecurityScopedResource()
        defer { if accessing { resolvedFolder.url.stopAccessingSecurityScopedResource() } }
        do {
            let bookmark = try BookmarkService.createBookmark(for: candidate.fileURL)
            clips[index].previousRef = clips[index].ref
            clips[index].ref = SourceRef(kind: .file, bookmarkData: bookmark, photosAssetID: nil)
            clips[index].fingerprint = candidate.fingerprint
            clips[index].debugBroken = nil
            save()
            availability[clipID] = check(clips[index])
        } catch {
            lastError = "Relink failed: \(error.localizedDescription)"
        }
    }

    func undoRelink(clipID: UUID) {
        guard let index = clips.firstIndex(where: { $0.id == clipID }),
              let previous = clips[index].previousRef else { return }
        clips[index].ref = previous
        clips[index].previousRef = nil
        save()
        availability[clipID] = check(clips[index])
    }

    // MARK: - Debug + deletion

    func toggleSimulatedBroken(clipID: UUID) {
        guard let index = clips.firstIndex(where: { $0.id == clipID }) else { return }
        clips[index].debugBroken = clips[index].isDebugBroken ? nil : true
        save()
        availability[clipID] = check(clips[index])
    }

    func deleteClips(at offsets: IndexSet) {
        for index in offsets {
            ThumbnailService.delete(for: clips[index].id)
        }
        clips.remove(atOffsets: offsets)
        save()
    }
}
