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] = []
    /// The project (or "category") this note is filed under, nil when unfiled.
    var project: 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?
    // Latest transcription attempt, cached flat for list display and search.
    // The full attempt history lives only in the file (like `transcribedAt`).
    var transcriptText: String?
    var transcriptConfidence: Double?

    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` and the attempt history are not fully cached —
    /// only the latest attempt survives (with a placeholder date), acceptable
    /// for the cache-only offline fallback, where the file (which carries the
    /// full history) simply wasn't reachable.
    var audio: Note.AudioInfo? {
        get {
            guard hasAudio else { return nil }
            var attempts: [Note.TranscriptionAttempt] = []
            if let transcriptText {
                attempts = [Note.TranscriptionAttempt(
                    text: transcriptText,
                    localeID: transcriptionLocaleID ?? "",
                    confidence: transcriptConfidence,
                    transcribedAt: modifiedAt,
                    languageWasForced: false
                )]
            }
            return Note.AudioInfo(
                duration: audioDuration ?? 0,
                transcriberDeviceID: transcriberDeviceID ?? "",
                transcriptionStatus: transcriptionStatusRaw
                    .flatMap(Note.TranscriptionStatus.init(rawValue:)) ?? .pending,
                transcriptionLocaleID: transcriptionLocaleID,
                transcribedAt: nil,
                attempts: attempts
            )
        }
        set {
            hasAudio = newValue != nil
            audioDuration = newValue?.duration
            transcriberDeviceID = newValue?.transcriberDeviceID
            transcriptionStatusRaw = newValue?.transcriptionStatus.rawValue
            transcriptionLocaleID = newValue?.transcriptionLocaleID
            transcriptText = newValue?.latestAttempt?.text
            transcriptConfidence = newValue?.latestAttempt?.confidence
        }
    }

    /// What a reader should see as this note's body: the user's own text when
    /// they wrote any, else the latest transcript.
    var displayText: String {
        text.isEmpty ? (transcriptText ?? "") : text
    }

    /// First line of the display text, used as the display title.
    var title: String {
        displayText.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 }
}
