(** cn.ml — CLI entrypoint, thin dispatch (native OCaml binary)

    DESIGN: This is the CLI dispatch layer. It parses args,
    finds the hub, and routes to the right module.

    Layering (deliberate):
      cn.ml         → Dispatch (THIS FILE — ~100 lines)
      cn_runtime.ml → Agent runtime orchestrator (dequeue → LLM → execute)
      cn_context.ml → Context packer (skills, conversation, artifacts)
      cn_llm.ml     → Claude API client (curl-backed)
      cn_telegram.ml→ Telegram Bot API client
      cn_config.ml  → Config loader (env vars + .cn/config.json)
      cn_agent.ml   → Agent I/O, queue, execution
      cn_mail.ml    → Inbox/outbox mail transport
      cn_gtd.ml     → Thread lifecycle + creation
      cn_mca.ml     → Managed Concern Aggregation
      cn_commands.ml→ Peers + git commands
      cn_system.ml  → Setup, update, init
      cn_hub.ml     → Hub discovery, paths, helpers
      cn_fmt.ml     → Output formatting
      cn_ffi.ml     → Native system bindings (stdlib + Unix)
      cn_lib.ml     → Types, parsing (pure)
      cn_json.ml    → JSON parser/emitter (pure) *)

open Cn_lib

let drop n lst =
  let rec go n lst = match n, lst with
    | 0, lst -> lst
    | _, [] -> []
    | n, _ :: rest -> go (n - 1) rest
  in go n lst

let () =
  Cn_system.self_update_check ();

  let args = Cn_ffi.Process.argv |> Array.to_list |> drop 1 in
  let flags, cmd_args = parse_flags args in
  Cn_fmt.dry_run_mode := flags.dry_run;
  if flags.dry_run then Cn_fmt.dry_run_banner ();

  match parse_command cmd_args with
  | None ->
      (* No built-in matched: try external command discovery
         (repo-local, then vendored package). parse_command returns
         Some Help on [], so cmd_args is guaranteed non-empty here. *)
      let cmd_name, rest = match cmd_args with
        | c :: r -> c, r
        | [] -> assert false
      in
      let unknown () =
        print_endline (Cn_fmt.fail
          (Printf.sprintf "Unknown command: %s" cmd_name));
        Cn_help.run_help ();
        Cn_ffi.Process.exit 1
      in
      (match Cn_hub.discover (Cn_ffi.Process.cwd ()) with
       | None -> unknown ()
       | Some placement ->
           let hub_path = placement.Cn_placement.hub_root in
           match Cn_command.find ~hub_path cmd_name with
           | None -> unknown ()
           | Some cmd ->
               let code = Cn_command.dispatch cmd ~hub_path ~args:rest in
               Cn_ffi.Process.exit code)
  | Some Help -> Cn_help.run_help ()
  | Some Version -> Printf.printf "cn %s (%s)\n" version cnos_commit
  | Some Update ->
      (match Cn_hub.discover (Cn_ffi.Process.cwd ()) with
       | Some p -> Cn_system.run_update_in_hub p.Cn_placement.hub_root
       | None -> Cn_system.run_update None)
  | Some (Init name) -> Cn_system.run_init name
  | Some (Build Cn_lib.Build.Packages) -> Cn_build.run_build ()
  | Some (Build Cn_lib.Build.Check) -> Cn_build.run_check ()
  | Some (Build Cn_lib.Build.Clean) -> Cn_build.run_clean ()
  | Some cmd ->
      match Cn_hub.discover (Cn_ffi.Process.cwd ()) with
      | None ->
          print_endline (Cn_fmt.fail "Not in a cn hub.");
          print_endline "";
          print_endline "Either:";
          print_endline "  1) cd into an existing hub (cn-sigma, cn-pi, etc.)";
          print_endline "  2) cn init <name> to create a new one";
          Cn_ffi.Process.exit 1
      | Some placement ->
          (* #156: hub_root for state/threads/config, workspace_root for
             project fs/git ops. Currently all callers use hub_path —
             workspace_root threading happens incrementally. *)
          let hub_path = placement.Cn_placement.hub_root in
          let _workspace_root = placement.Cn_placement.workspace_root in
          let name = derive_name hub_path in
          match cmd with
          | Status -> Cn_system.run_status hub_path name
          | Doctor -> Cn_doctor.run_doctor ~hub_path
          | Inbox Inbox.Check -> Cn_mail.inbox_check hub_path name
          | Inbox Inbox.Process -> Cn_mail.inbox_process hub_path
          | Inbox Inbox.Flush -> Cn_mail.inbox_flush hub_path name
          | Outbox Outbox.Check -> Cn_mail.outbox_check hub_path
          | Outbox Outbox.Flush -> Cn_mail.outbox_flush hub_path name
          | Sync -> Cn_system.run_sync hub_path name
          | Next -> Cn_mail.run_next hub_path
          | Agent mode ->
              (match Cn_config.load ~hub_path with
               | Error msg ->
                   print_endline (Cn_fmt.fail (Printf.sprintf "Config error: %s" msg));
                   Cn_ffi.Process.exit 1
               | Ok config ->
                   match mode with
                   | Agent.Cron -> Cn_runtime.run_cron ~config ~hub_path ~name
                   | Agent.Process ->
                       (match Cn_runtime.process_one ~config ~hub_path ~name with
                        | Ok () -> ()
                        | Error msg ->
                            print_endline (Cn_fmt.fail msg);
                            Cn_ffi.Process.exit 1)
                   | Agent.Daemon -> Cn_runtime.run_daemon ~config ~hub_path ~name
                   | Agent.Stdio -> Cn_runtime.run_stdio ~config ~hub_path ~name)
          | Read t -> Cn_gtd.run_read hub_path t
          | Reply (t, m) -> Cn_commands.run_reply hub_path t m
          | Send (p, m) -> Cn_commands.run_send hub_path p m
          | Gtd (Gtd.Delete t) -> Cn_gtd.gtd_delete hub_path t
          | Gtd (Gtd.Defer (t, u)) -> Cn_gtd.gtd_defer hub_path t u
          | Gtd (Gtd.Delegate (t, p)) -> Cn_gtd.gtd_delegate hub_path name t p
          | Gtd (Gtd.Do t) -> Cn_gtd.gtd_do hub_path t
          | Gtd (Gtd.Done t) -> Cn_gtd.gtd_done hub_path t
          | Commit msg -> Cn_commands.run_commit hub_path name msg
          | Push -> Cn_commands.run_push hub_path
          (* Save removed in v3.37.0 (#184) — resolved via external
             command dispatch to packages/cnos.core/commands/save/cn-save. *)
          | Peer Peer.List -> Cn_commands.run_peer_list hub_path
          | Peer (Peer.Add (n, url)) -> Cn_commands.run_peer_add hub_path n url
          | Peer (Peer.Remove n) -> Cn_commands.run_peer_remove hub_path n
          | Peer Peer.Sync -> Cn_commands.run_peer_sync hub_path
          | Queue Queue.List -> Cn_agent.run_queue_list hub_path
          | Queue Queue.Clear -> Cn_agent.run_queue_clear hub_path
          | Mca Mca.List -> Cn_mca.run_mca_list hub_path
          | Mca (Mca.Add desc) -> Cn_mca.run_mca_add hub_path name desc
          | Out gtd -> Cn_agent.run_out hub_path name gtd
          | Adhoc title -> Cn_gtd.run_adhoc hub_path title
          (* Daily / Weekly removed in v3.37.0 (#184) — external
             package commands, dispatched via Cn_command.find →
             dispatch when the built-in parser returns None. *)
          | Setup -> Cn_system.run_setup hub_path
          | Deps Deps.List -> Cn_deps.run_list ~hub_path
          | Deps Deps.Restore -> Cn_deps.run_restore ~hub_path
          | Deps Deps.Doctor -> Cn_deps.run_doctor ~hub_path
          | Deps (Deps.Add pkg) -> Cn_deps.run_add ~hub_path pkg
          | Deps (Deps.Remove pkg) -> Cn_deps.run_remove ~hub_path pkg
          | Deps (Deps.Update _pkg_opt) ->
              print_endline (Cn_fmt.info
                "deps update: re-resolve not yet implemented (v3.4.1)")
          | Deps Deps.Vendor -> Cn_deps.run_vendor ~hub_path
          | Logs (Logs.Show args) -> Cn_logs.run_logs hub_path args
          | Help | Version | Init _ | Update | Build _ -> ()
