import Foundation

/// Document list row. Non-optional set is intentional (B-2 review): only structural/envelope-grade
/// fields whose absence is a contract violation are non-optional, so a malformed response throws
/// loudly rather than silently nil-ing. `file_format` and `read_count` are OPTIONAL + fallback
/// (a note has no format; a light projection may omit the counter — one production null must not
/// kill the whole list decode). Everything else is optional (all-optional discipline).
public struct DocumentResponse: Codable, Sendable, Identifiable, Equatable {
    // Invariants (loud-throw on absence)
    public let id: Int
    public let fileType: String
    public let createdAtRaw: String
    public let updatedAtRaw: String
    // Optional + fallback
    public let fileFormat: String?
    public let readCount: Int?
    // Everything else optional
    public let filePath: String?
    public let fileSize: Int?
    public let title: String?
    public let aiDomain: String?
    public let aiSubGroup: String?
    public let aiTags: [String]?
    public let aiSummary: String?
    public let documentType: String?
    public let importance: String?
    public let aiConfidence: Double?
    public let userNote: String?
    public let userTags: [String]?
    public let pinned: Bool?
    public let askIncludable: Bool?
    public let derivedPath: String?
    public let originalFormat: String?
    public let conversionStatus: String?
    public let isRead: Bool?
    public let reviewStatus: String?
    public let editUrl: String?
    public let previewStatus: String?
    public let sourceChannel: String?
    public let dataOrigin: String?
    public let docPurpose: String?
    public let facetCompany: String?
    public let facetTopic: String?
    public let facetYear: Int?
    public let facetDoctype: String?
    public let category: String?
    public let aiSuggestion: JSONValue?
    public let aiTldr: String?
    public let aiBullets: [String]?
    public let aiDetailSummary: String?
    public let aiInconsistencies: [String]?
    public let aiAnalysisTier: String?
    public let extractedAtRaw: String?
    public let aiProcessedAtRaw: String?
    public let embeddedAtRaw: String?
    public let lastReadAtRaw: String?
    // [S1-ADD]
    public let originalFilename: String?
    public let duplicateOf: Int?
    public let duplicateCount: Int?

    public var createdAt: Date? { DSDate.parse(createdAtRaw) }
    public var updatedAt: Date? { DSDate.parse(updatedAtRaw) }
    public var lastReadAt: Date? { DSDate.parse(lastReadAtRaw) }
    public var reads: Int { readCount ?? 0 }
    public var displayFormat: String {
        fileFormat ?? filePath.map { ($0 as NSString).pathExtension }.flatMap { $0.isEmpty ? nil : $0 } ?? "?"
    }
    public var downloadLabel: String {
        originalFilename ?? filePath.map { ($0 as NSString).lastPathComponent } ?? "원본"
    }
    public var hasDownloadableOriginal: Bool { fileType != "note" }

    enum CodingKeys: String, CodingKey {
        case id, title, importance, pinned, category
        case fileType = "file_type"
        case createdAtRaw = "created_at"
        case updatedAtRaw = "updated_at"
        case fileFormat = "file_format"
        case readCount = "read_count"
        case filePath = "file_path"
        case fileSize = "file_size"
        case aiDomain = "ai_domain"
        case aiSubGroup = "ai_sub_group"
        case aiTags = "ai_tags"
        case aiSummary = "ai_summary"
        case documentType = "document_type"
        case aiConfidence = "ai_confidence"
        case userNote = "user_note"
        case userTags = "user_tags"
        case askIncludable = "ask_includable"
        case derivedPath = "derived_path"
        case originalFormat = "original_format"
        case conversionStatus = "conversion_status"
        case isRead = "is_read"
        case reviewStatus = "review_status"
        case editUrl = "edit_url"
        case previewStatus = "preview_status"
        case sourceChannel = "source_channel"
        case dataOrigin = "data_origin"
        case docPurpose = "doc_purpose"
        case facetCompany = "facet_company"
        case facetTopic = "facet_topic"
        case facetYear = "facet_year"
        case facetDoctype = "facet_doctype"
        case aiSuggestion = "ai_suggestion"
        case aiTldr = "ai_tldr"
        case aiBullets = "ai_bullets"
        case aiDetailSummary = "ai_detail_summary"
        case aiInconsistencies = "ai_inconsistencies"
        case aiAnalysisTier = "ai_analysis_tier"
        case extractedAtRaw = "extracted_at"
        case aiProcessedAtRaw = "ai_processed_at"
        case embeddedAtRaw = "embedded_at"
        case lastReadAtRaw = "last_read_at"
        case originalFilename = "original_filename"
        case duplicateOf = "duplicate_of"
        case duplicateCount = "duplicate_count"
    }
}

/// Single-document detail = the flat DocumentResponse fields + markdown/body fields, decoded from the
/// same flat JSON object via composition (avoids duplicating ~45 field declarations). The pending-md
/// fixture decodes through the SAME struct.
public struct DocumentDetailResponse: Decodable, Sendable, Identifiable {
    public let base: DocumentResponse
    public let extractedText: String?
    public let mdContent: String?
    public let mdFrontmatter: JSONValue?
    public let mdStatus: String?
    public let mdExtractionQuality: JSONValue?
    public let mdExtractionError: String?
    public let mdExtractionEngine: String?
    public let mdExtractionEngineVersion: String?
    public let mdGeneratedAtRaw: String?

    public var id: Int { base.id }
    public var mdGeneratedAt: Date? { DSDate.parse(mdGeneratedAtRaw) }
    /// MD-first render rule: render md_content when status is completed/partial, else fall back.
    public var mdIsRenderable: Bool { mdStatus == "completed" || mdStatus == "partial" }

    enum CodingKeys: String, CodingKey {
        case extractedText = "extracted_text"
        case mdContent = "md_content"
        case mdFrontmatter = "md_frontmatter"
        case mdStatus = "md_status"
        case mdExtractionQuality = "md_extraction_quality"
        case mdExtractionError = "md_extraction_error"
        case mdExtractionEngine = "md_extraction_engine"
        case mdExtractionEngineVersion = "md_extraction_engine_version"
        case mdGeneratedAtRaw = "md_generated_at"
    }

    public init(from decoder: Decoder) throws {
        self.base = try DocumentResponse(from: decoder)
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.extractedText = try c.decodeIfPresent(String.self, forKey: .extractedText)
        self.mdContent = try c.decodeIfPresent(String.self, forKey: .mdContent)
        self.mdFrontmatter = try c.decodeIfPresent(JSONValue.self, forKey: .mdFrontmatter)
        self.mdStatus = try c.decodeIfPresent(String.self, forKey: .mdStatus)
        self.mdExtractionQuality = try c.decodeIfPresent(JSONValue.self, forKey: .mdExtractionQuality)
        self.mdExtractionError = try c.decodeIfPresent(String.self, forKey: .mdExtractionError)
        self.mdExtractionEngine = try c.decodeIfPresent(String.self, forKey: .mdExtractionEngine)
        self.mdExtractionEngineVersion = try c.decodeIfPresent(String.self, forKey: .mdExtractionEngineVersion)
        self.mdGeneratedAtRaw = try c.decodeIfPresent(String.self, forKey: .mdGeneratedAtRaw)
    }
}

public struct DocumentListResponse: Codable, Sendable {
    public let items: [DocumentResponse]
    public let total: Int
    public let page: Int
    public let pageSize: Int

    enum CodingKeys: String, CodingKey {
        case items, total, page
        case pageSize = "page_size"
    }
}

public struct DocumentContentResponse: Codable, Sendable {
    public let id: Int
    public let title: String?
    public let domain: String?
    public let subGroup: String?
    public let documentType: String?
    public let aiSummary: String?
    public let aiTags: [String]?
    public let content: String?
    public let contentLength: Int?
    public let truncated: Bool?

    enum CodingKeys: String, CodingKey {
        case id, title, domain, content, truncated
        case subGroup = "sub_group"
        case documentType = "document_type"
        case aiSummary = "ai_summary"
        case aiTags = "ai_tags"
        case contentLength = "content_length"
    }
}
