import ArgumentParser
import Darwin
import Diagnostics
import Foundation
import Search
import SharedConstants

// MARK: - Cupertino CLI

@main
@available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
struct Cupertino: AsyncParsableCommand {
    static let configuration = CommandConfiguration(
        commandName: Shared.Constants.App.commandName,
        abstract: "MCP Server for Apple Documentation, Swift Evolution, and Swift Packages",
        discussion: """
        SETUP
          setup            Download pre-built databases (search.db, samples.db, packages.db)

        DATA COLLECTION
          fetch            Download documentation, packages, and sample code from Apple / GitHub
          cleanup          Remove downloaded sample-code archives after indexing

        INDEXING
          save             Index fetched content into search.db, packages.db, and samples.db

        SERVER
          serve            Start the MCP server (default when no subcommand is given)

        QUERY
          search           Search across all documentation sources
          package-search   Smart query over the packages corpus (packages.db source only)
          read             Read a full document by URI
          list-frameworks  List indexed frameworks with document counts
          inheritance      Walk class inheritance chains (Apple class-based APIs)

        SAMPLE CODE
          list-samples     List indexed Apple sample projects
          read-sample      Read a sample project README and metadata
          read-sample-file Read a source file from a sample project

        DIAGNOSTICS
          doctor           Check server health, database state, and save readiness
          resolve-refs     Rewrite unresolved doc:// markers in saved page content

        Run 'cupertino <command> --help' for per-command options.
        """,
        version: Shared.Constants.App.version,
        subcommands: [
            CLIImpl.Command.Setup.self,
            CLIImpl.Command.Fetch.self,
            CLIImpl.Command.Save.self,
            CLIImpl.Command.Serve.self,
            CLIImpl.Command.Search.self,
            CLIImpl.Command.Read.self,
            CLIImpl.Command.ListFrameworks.self,
            CLIImpl.Command.ListSamples.self,
            CLIImpl.Command.ReadSample.self,
            CLIImpl.Command.ReadSampleFile.self,
            CLIImpl.Command.Doctor.self,
            CLIImpl.Command.Cleanup.self,
            CLIImpl.Command.PackageSearch.self,
            CLIImpl.Command.ResolveRefs.self,
            CLIImpl.Command.Inheritance.self,
        ],
        defaultSubcommand: CLIImpl.Command.Serve.self
    )

    /// Force stdout to line-buffered mode before any subcommand runs. By
    /// default, libc switches stdout to block-buffered (4–8 KB) when it
    /// detects a non-tty (i.e. piped to `tee`, redirected to a file). That
    /// makes long-running fetches appear hung — output piles up inside the
    /// process for minutes before flushing. Line-buffered means every `\n`
    /// flushes immediately, which is what the user expects.
    ///
    /// #548 Phase B: build one `Cupertino.Composition` (Mediator, GoF
    /// p. 273) and bind it via `@TaskLocal` for the lifetime of the
    /// program. Subcommand `run()` bodies read the binding and thread
    /// `composition.recording` / `composition.paths` into producers as
    /// explicit parameters. The TaskLocal is structurally scoped to
    /// this `withValue { … }` block — not a Singleton.
    static func main() async {
        setvbuf(stdout, nil, _IOLBF, 0)

        let composition = Cupertino.Composition()
        do {
            try await Cupertino.Context.$composition.withValue(composition) {
                // Replicates the default `AsyncParsableCommand.main()` body
                // so the override doesn't lose any behaviour.
                var command = try parseAsRoot()
                if var asyncCommand = command as? AsyncParsableCommand {
                    try await asyncCommand.run()
                } else {
                    try command.run()
                }
            }
        } catch let mismatch as SearchModule.Error where Self.isSchemaMismatch(mismatch) {
            // #673 Phase E — schema-version mismatch is a known, recoverable
            // class of error with a concrete remediation. Print the user-
            // friendly `errorDescription` (NO Swift stack trace) on stderr,
            // then exit with `EX_DATAERR` (65 — "the input data was incorrect
            // in some way", per sysexits(3)) so scripts can detect this class
            // without parsing the message text.
            //
            // Distinct from `exit(withError:)`'s default mapping (which would
            // print the underlying error's String description + exit with
            // generic `EXIT_FAILURE` = 1).
            FileHandle.standardError.write(Data((mismatch.errorDescription ?? "Schema version mismatch.\n").utf8))
            FileHandle.standardError.write(Data("\n".utf8))
            Darwin.exit(Int32(EX_DATAERR))
        } catch let lowDisk as Diagnostics.InsufficientDiskSpaceError {
            // #673 Phase F — disk-space preflight refused the operation.
            // Print the typed `errorDescription` (which already names
            // the path, needed, free, short-by figures) and exit with
            // `EX_IOERR` (74 — sysexits(3) "an error occurred while
            // doing I/O on some file") so scripts / CI / agent wrappers
            // can detect the class. Distinct from the schema-mismatch
            // exit code so a single catch-all in a deployer script can
            // tell the difference (`exit 65 → cupertino setup` vs
            // `exit 74 → free disk + retry`).
            FileHandle.standardError.write(Data((lowDisk.errorDescription ?? "Insufficient disk space.\n").utf8))
            FileHandle.standardError.write(Data("\n".utf8))
            Darwin.exit(Int32(EX_IOERR))
        } catch {
            exit(withError: error)
        }
    }

    /// True when the value is the `.schemaVersionMismatch` case. Helper
    /// so the `catch let … where` clause stays readable.
    private static func isSchemaMismatch(_ error: SearchModule.Error) -> Bool {
        if case .schemaVersionMismatch = error { return true }
        return false
    }
}
