import Foundation
import SwiftData

/// Rebuildable cache row for one note. Holds nothing that isn't also, more
/// authoritatively, in the note's JSON file — the metadata observer (via
/// `NoteMapper`) is the only writer.
@Model
final class NoteEntity {
    @Attribute(.unique) var id: String = ""
    var text: String = ""
    var sourceRaw: String = Note.Source.typed.rawValue
    var tags: [String] = []
    var createdAt: Date = Date()
    var modifiedAt: Date = Date()

    /// Back-pointer to the authoritative file, so a "removed" event can find
    /// the row to delete by path when it lacks the id.
    var jsonRelativePath: String = ""

    // Flattened CaptureContext — @Model can't store a nested Codable struct
    // as one column; call sites use the `context` computed property instead.
    var latitude: Double?
    var longitude: Double?
    var horizontalAccuracy: Double?
    var placeName: String?
    var thoroughfare: String?
    var locality: String?
    var administrativeArea: String?
    var country: String?
    var timeZoneID: String?
    var device: String?

    // Flattened AudioInfo — nil columns mean a plain text note. `hasAudio` is
    // stored rather than derived from `audioDuration` so a zero-length recording
    // is still recognizably a voice note. `transcribedAt` is deliberately not
    // cached (the file is authoritative for that timestamp), so a cache-only
    // reconstruction of a completed note has a nil `transcribedAt`.
    var hasAudio: Bool = false
    var audioDuration: Double?
    var transcriptionStatusRaw: String?
    var transcriptionLocaleID: String?
    var transcriberDeviceID: String?

    init(id: String, jsonRelativePath: String) {
        self.id = id
        self.jsonRelativePath = jsonRelativePath
    }
}

extension NoteEntity {
    var context: CaptureContext {
        get {
            CaptureContext(
                latitude: latitude,
                longitude: longitude,
                horizontalAccuracy: horizontalAccuracy,
                placeName: placeName,
                thoroughfare: thoroughfare,
                locality: locality,
                administrativeArea: administrativeArea,
                country: country,
                timeZoneID: timeZoneID,
                device: device
            )
        }
        set {
            latitude = newValue.latitude
            longitude = newValue.longitude
            horizontalAccuracy = newValue.horizontalAccuracy
            placeName = newValue.placeName
            thoroughfare = newValue.thoroughfare
            locality = newValue.locality
            administrativeArea = newValue.administrativeArea
            country = newValue.country
            timeZoneID = newValue.timeZoneID
            device = newValue.device
        }
    }

    var source: Note.Source {
        Note.Source(rawValue: sourceRaw) ?? .typed
    }

    /// Reconstructs `AudioInfo` from the flattened columns (nil for a text
    /// note). `transcribedAt` is not cached, so it comes back nil even for a
    /// completed transcription — acceptable for the cache-only offline fallback,
    /// where the file (which carries it) simply wasn't reachable.
    var audio: Note.AudioInfo? {
        get {
            guard hasAudio else { return nil }
            return Note.AudioInfo(
                duration: audioDuration ?? 0,
                transcriberDeviceID: transcriberDeviceID ?? "",
                transcriptionStatus: transcriptionStatusRaw
                    .flatMap(Note.TranscriptionStatus.init(rawValue:)) ?? .pending,
                transcriptionLocaleID: transcriptionLocaleID,
                transcribedAt: nil
            )
        }
        set {
            hasAudio = newValue != nil
            audioDuration = newValue?.duration
            transcriberDeviceID = newValue?.transcriberDeviceID
            transcriptionStatusRaw = newValue?.transcriptionStatus.rawValue
            transcriptionLocaleID = newValue?.transcriptionLocaleID
        }
    }

    /// First line of the text, used as the display title.
    var title: String {
        text.split(separator: "\n", omittingEmptySubsequences: true)
            .first.map(String.init) ?? ""
    }
}

extension PersistentModel {
    /// Safe-to-read guard for objects reached through `@Query` or a
    /// relationship during a rebuild/sync burst. `isDeleted` alone only
    /// covers the unsaved window.
    var isLive: Bool { modelContext != nil && !isDeleted }
}
