import Foundation

/// **The three formats v1 converts** (15-import-export.md ▸ The formats) — the export menu's three
/// rows, and the three answers an import's sniff can give.
///
/// Both directions in one enum rather than an export set and an import set, because the set is the
/// same set: every v1 format round-trips, which is what makes the round-trip suite the feature's
/// primary proof.
public enum InterchangeFormat: String, Sendable, Equatable, CaseIterable {

    /// The Obsidian Kanban plugin's board file (mgmeyers/obsidian-kanban): one `.md`, `kanban-plugin:
    /// board` frontmatter, `##` lanes, `- [ ]` cards.
    case obsidianKanban

    /// The same document minus the plugin marker — a plain Markdown outline anybody can read, paste
    /// into a PR, or hand-write.
    case markdownOutline

    /// One row per card: `lane,title,body,created,modified`, RFC 4180.
    case csv

    /// The name the menu row and the banner both use. Sentence-cased for the banner's mid-sentence
    /// position; the menu rows spell their own titles, because a menu title is API
    /// (`KanbanApp.menuCommands`) and must not be derived from anything that could be reworded.
    public var displayName: String {
        switch self {
        case .obsidianKanban: "Obsidian Kanban Markdown"
        case .markdownOutline: "Markdown outline"
        case .csv: "CSV"
        }
    }

    /// The extension a save panel suggests, and the one an exported file lands with.
    public var fileExtension: String {
        switch self {
        case .obsidianKanban, .markdownOutline: "md"
        case .csv: "csv"
        }
    }

    // MARK: - Detection

    /// **Which format a file is, decided from its bytes and its name** — the whole of File ▸ Import
    /// Board…'s format question, which the user is never asked (15 ▸ One import row, no format picker).
    ///
    /// The ladder, in order, and every rung is a fact rather than a guess where it can be:
    ///
    /// 1. **A `kanban-plugin` key in the leading frontmatter block** is the plugin's own marker and is
    ///    conclusive — this is the one positive identification any of the three formats offers.
    /// 2. **A `.csv` extension** is the user's own statement about their file, and outranks the shape
    ///    sniff below for the case the sniff is worst at: a one-column CSV with no commas in it.
    /// 3. **A Markdown extension** (`.md`, `.markdown`, `.mdown`, `.txt`) means outline, so a Markdown
    ///    document whose prose happens to be comma-heavy is never mistaken for a table.
    /// 4. **The shape sniff** — for a file with no useful extension at all (an emailed attachment, a
    ///    pasted-together dump): CSV only when the content has no Markdown structure *and* parses as a
    ///    consistent multi-column table (`looksLikeCSV`).
    /// 5. **Outline otherwise**, which is the lenient parser and therefore the safe default: it never
    ///    fails, and the worst it does with a genuinely odd file is land everything in one lane.
    ///
    /// Nothing here distinguishes `.obsidianKanban` from `.markdownOutline` beyond rung 1, and nothing
    /// needs to: the two parsers *are* one parser (`MarkdownBoardParser`), because the plugin's document
    /// is a plain outline wearing a frontmatter marker. The distinction is kept because the caller
    /// reports which format it detected and because the export side genuinely differs.
    public static func detect(text: String, fileName: String?) -> InterchangeFormat {
        if frontmatterCarriesKanbanPlugin(text) { return .obsidianKanban }

        let fileExtension = (fileName as NSString?)?.pathExtension.lowercased() ?? ""
        if fileExtension == "csv" { return .csv }
        if markdownExtensions.contains(fileExtension) { return .markdownOutline }

        return looksLikeCSV(text) ? .csv : .markdownOutline
    }

    private static let markdownExtensions: Set<String> = ["md", "markdown", "mdown", "mkd", "txt"]

    /// Whether the document opens with a YAML frontmatter block carrying a `kanban-plugin` key.
    ///
    /// **Scanned as lines, not parsed as YAML.** The plugin writes its block with blank lines inside the
    /// delimiters (`---`, ``, `kanban-plugin: board``, ``, `---`), a file may carry keys this app has
    /// never heard of, and a foreign document's frontmatter is under nobody's obligation to be
    /// well-formed. `FrontmatterDocument.parse` would answer this question by *refusing* the file, which
    /// is exactly the wrong outcome for a detector whose next fallback is a parser that never fails.
    static func frontmatterCarriesKanbanPlugin(_ text: String) -> Bool {
        guard let block = MarkdownBoardParser.frontmatterBlock(in: MarkdownBoardParser.lines(of: text)) else {
            return false
        }
        return block.contains { line in
            let key = line.prefix { $0 != ":" }
            return key.trimmingCharacters(in: .whitespaces) == "kanban-plugin"
        }
    }

    /// The shape sniff — deliberately narrow, because its only job is to catch an extension-less table
    /// and its cost of being wrong is a Markdown document shredded into one card per line.
    ///
    /// Three conditions, all required: **no Markdown structure at all** (no ATX heading and no bullet at
    /// column 0 — either one means outline, whatever else is in the file), **at least two records**, and
    /// **a consistent field count of two or more** across every record. A single-column list of words,
    /// a prose paragraph, and an empty file all fail it and fall through to the outline parser, which
    /// handles each of them sensibly.
    static func looksLikeCSV(_ text: String) -> Bool {
        let lines = MarkdownBoardParser.lines(of: text)
        for line in lines where MarkdownBoardParser.isMarkdownStructure(line) {
            return false
        }
        let records = CSVDocument.decode(text)
        guard records.count >= 2, let width = records.first?.count, width >= 2 else { return false }
        return records.allSatisfy { $0.count == width }
    }
}
