//
// ExerciseInfo.swift
// Workouts
//
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
//

import Foundation

/// Parsed form of an exercise's authored `info.md` (see `Exercise Library/SYSTEM.md`):
/// a one-paragraph summary, the Category/Type/Targets metadata bullets, then the
/// instructional sections (Setup, Execution, Cues, Common Mistakes, Progression…).
/// The library's markdown is uniform, so this parses the known shape rather than
/// general Markdown — SwiftUI's inline-Markdown `Text` can't render headers/lists,
/// and the app wants the structure (target chips, numbered steps) anyway.
struct ExerciseInfo {
    /// The intro paragraph(s) between the `# Title` and the metadata bullets.
    var summary: String
    var category: String?
    var type: String?
    /// The comma-separated `**Targets:**` values, split for chip rendering.
    var targets: [String]
    var sections: [Section]
    /// Parsed `**Defaults:**` bullet — the library's suggested starting plan for an
    /// exercise added outside a routine (no prior log, no routine entry to copy from).
    /// `nil` when the bullet is absent (not yet authored) or unparseable.
    var defaults: Defaults?

    /// Whether the authored `Type:` identifies this as a machine exercise — the
    /// library convention is that every machine entry's type starts "Machine-based".
    /// Drives the machine-settings sections' visibility before any settings exist.
    var isMachineBased: Bool {
        type?.localizedCaseInsensitiveContains("machine") == true
    }

    struct Section {
        var title: String
        var items: [Item]
    }

    /// The library's suggested starting plan, parsed from the `**Defaults:**` bullet.
    struct Defaults {
        var sets: Int
        var reps: Int
        var loadType: LoadType
        var durationSeconds: Int
    }

    /// One block within a section, preserving the authored list flavor: numbered
    /// steps render with their ordinal, bullets with a dot, paragraphs plain.
    enum Item {
        case step(String)
        case bullet(String)
        case paragraph(String)

        var text: String {
            switch self {
            case .step(let s), .bullet(let s), .paragraph(let s): s
            }
        }
    }
}

extension ExerciseInfo {
    /// Sections read aloud in the brief hands-free cue (the workout-flow announcement).
    /// The library's Speak button reads everything; a mid-workout cue skips the reference
    /// sections (Common Mistakes, Progression) you don't need with the bar in your hands.
    private static let briefSectionTitles: Set<String> = ["Setup", "Execution", "Cues"]

    /// Render this exercise as a plain-text script for `AVSpeechSynthesizer`: the name,
    /// the summary, then each section's title and items as sentences. `brief` keeps only
    /// the hands-free sections (`briefSectionTitles`), falling back to every section when
    /// none of them are present, so an exercise with only unusual headings still speaks.
    func spokenScript(name: String, brief: Bool) -> String {
        var parts: [String] = [name]

        let summaryLine = summary.replacingOccurrences(of: "\n", with: " ")
        if !summaryLine.trimmingCharacters(in: .whitespaces).isEmpty {
            parts.append(summaryLine)
        }

        var chosen = brief ? sections.filter { Self.briefSectionTitles.contains($0.title) } : sections
        if chosen.isEmpty { chosen = sections }

        for section in chosen {
            parts.append(section.title)
            parts.append(contentsOf: section.items.map(\.text))
        }

        return parts
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }
            .map { $0.hasSuffix(".") || $0.hasSuffix("!") || $0.hasSuffix("?") ? $0 : $0 + "." }
            .joined(separator: " ")
    }

    /// Parse the library's `info.md` shape. Hard-wrapped lines are rejoined: a line
    /// that doesn't start a new block (heading, list marker, blank) continues the
    /// previous one, matching how Markdown renders soft breaks.
    static func parse(markdown: String) -> ExerciseInfo {
        var summaryLines: [String] = []
        var category: String?
        var type: String?
        var targets: [String] = []
        var defaults: Defaults?
        var sections: [Section] = []

        var currentSection: Section?
        var currentItem: Item?

        func metadataValue(_ line: String, key: String) -> String? {
            let prefix = "- **\(key):**"
            guard line.hasPrefix(prefix) else { return nil }
            return String(line.dropFirst(prefix.count)).trimmingCharacters(in: .whitespaces)
        }

        func flushItem() {
            guard let item = currentItem else { return }
            currentItem = nil
            if currentSection != nil {
                currentSection?.items.append(item)
            } else if case .paragraph(let text) = item {
                summaryLines.append(text)
            }
        }

        func flushSection() {
            flushItem()
            if let section = currentSection { sections.append(section) }
            currentSection = nil
        }

        func append(to item: Item?, _ text: String) -> Item? {
            switch item {
            case .step(let s): .step(s + " " + text)
            case .bullet(let s): .bullet(s + " " + text)
            case .paragraph(let s): .paragraph(s + " " + text)
            case nil: .paragraph(text)
            }
        }

        for rawLine in markdown.components(separatedBy: .newlines) {
            let line = rawLine.trimmingCharacters(in: .whitespaces)

            if line.isEmpty {
                flushItem()
            } else if line.hasPrefix("## ") {
                flushSection()
                currentSection = Section(title: String(line.dropFirst(3)), items: [])
            } else if line.hasPrefix("# ") {
                continue // the document title duplicates the exercise name
            } else if currentSection == nil,
                      let value = metadataValue(line, key: "Category") {
                flushItem()
                category = value
            } else if currentSection == nil,
                      let value = metadataValue(line, key: "Type") {
                flushItem()
                type = value
            } else if currentSection == nil,
                      let value = metadataValue(line, key: "Targets") {
                flushItem()
                targets = value.components(separatedBy: ",")
                    .map { $0.trimmingCharacters(in: .whitespaces) }
                    .filter { !$0.isEmpty }
            } else if currentSection == nil,
                      let value = metadataValue(line, key: "Defaults") {
                flushItem()
                defaults = parseDefaults(value)
            } else if let range = line.range(of: #"^\d+\.\s+"#, options: .regularExpression) {
                flushItem()
                currentItem = .step(String(line[range.upperBound...]))
            } else if line.hasPrefix("- ") {
                flushItem()
                currentItem = .bullet(String(line.dropFirst(2)))
            } else {
                // Continuation of a hard-wrapped block (or the start of a paragraph).
                currentItem = append(to: currentItem, line)
            }
        }
        flushSection()

        return ExerciseInfo(
            summary: summaryLines.joined(separator: "\n\n"),
            category: category,
            type: type,
            targets: targets,
            sections: sections,
            defaults: defaults
        )
    }

    /// Parse a `**Defaults:**` value — "4 × 10 weighted", "3 × 12 bodyweight", or
    /// "3 × 30 s" — into sets/reps/loadType. The trailing unit picks the `LoadType`
    /// and which of reps/duration the middle number fills; anything else (missing
    /// bullet, unrecognized unit) yields `nil` rather than guessing.
    private static func parseDefaults(_ value: String) -> Defaults? {
        let parts = value.components(separatedBy: " × ")
        guard parts.count == 2, let sets = Int(parts[0]) else { return nil }
        let rest = parts[1].components(separatedBy: " ")
        guard rest.count == 2, let n = Int(rest[0]) else { return nil }
        switch rest[1] {
        case "weighted":
            return Defaults(sets: sets, reps: n, loadType: .weight, durationSeconds: 0)
        case "bodyweight":
            return Defaults(sets: sets, reps: n, loadType: .none, durationSeconds: 0)
        case "s":
            return Defaults(sets: sets, reps: 0, loadType: .duration, durationSeconds: n)
        default:
            return nil
        }
    }
}

/// Finds and parses the bundled `<Exercise Name>.info.md` exported next to the
/// motion rigs by `render.py --export`.
enum ExerciseInfoLibrary {
    /// Every bundled `info.md`, parsed once and keyed by exercise name. `info(for:)` is
    /// fanned out per exercise per keystroke by `ExerciseCatalog.sections(matching:)`, so
    /// re-doing the `Bundle.main` lookup, file read, and markdown parse on every call was
    /// wasteful; ~60-odd files parsed once is trivial. Enumerates the bundle directly
    /// (mirroring `ExerciseMotionLibrary.exerciseNames`'s compound-suffix filter) rather
    /// than keying off that list, so info lookup doesn't depend on a motion rig existing.
    private static let cache: [String: ExerciseInfo] = {
        let urls = Bundle.main.urls(forResourcesWithExtension: "md", subdirectory: nil) ?? []
        var result: [String: ExerciseInfo] = [:]
        for url in urls {
            let filename = url.lastPathComponent
            guard filename.hasSuffix(".info.md"),
                  let markdown = try? String(contentsOf: url, encoding: .utf8)
            else { continue }
            let exerciseName = String(filename.dropLast(".info.md".count))
            result[exerciseName] = ExerciseInfo.parse(markdown: markdown)
        }
        return result
    }()

    /// The parsed info for `exerciseName`, or `nil` when none is bundled.
    static func info(for exerciseName: String) -> ExerciseInfo? {
        cache[exerciseName]
    }
}
