(** cn_system.ml — System setup, updates, and hub initialization

    System lifecycle: status, doctor, setup, update, sync, init.
    Merged from: cn_system + cn_init in the 14-module plan. *)

open Cn_lib

(* === Runtime === *)

let update_runtime hub_path =
  let runtime_path = Cn_ffi.Path.join hub_path "state/runtime.md" in
  let cn_ver = version in
  let hub_name = derive_name hub_path in
  let hub_commit = match Cn_ffi.Child_process.exec_in ~cwd:hub_path "git rev-parse --short HEAD 2>/dev/null" with
    | Some c -> String.trim c
    | None -> "unknown"
  in
  let cnos_commit = Cn_lib.cnos_commit in
  let peers_path = Cn_ffi.Path.join hub_path "state/peers.md" in
  let peer_count = match Cn_ffi.Fs.exists peers_path with
    | true ->
        let content = Cn_ffi.Fs.read peers_path in
        let lines = String.split_on_char '\n' content in
        lines |> List.filter (fun l -> String.length l > 2 && String.sub l 0 2 = "- ") |> List.length
    | false -> 0
  in
  (* Read installed package versions for drift detection *)
  let pkg_versions =
    let installed = Cn_assets.list_installed_packages hub_path in
    installed |> List.map (fun (_name, pkg_path) ->
      let dir_name = Filename.basename pkg_path in
      match String.index_opt dir_name '@' with
      | Some i ->
        let n = String.sub dir_name 0 i in
        let v = String.sub dir_name (i + 1) (String.length dir_name - i - 1) in
        Printf.sprintf "%s@%s" n v
      | None -> dir_name)
  in
  let pkg_line = match pkg_versions with
    | [] -> "packages: (none)"
    | vs -> "packages: " ^ String.concat ", " vs
  in
  let drift = pkg_versions |> List.exists (fun pv ->
    match String.index_opt pv '@' with
    | Some i ->
      let v = String.sub pv (i + 1) (String.length pv - i - 1) in
      v <> cn_ver
    | None -> false)
  in
  let content = Printf.sprintf {|# Runtime State

Auto-generated by cn. Do not edit manually.

```yaml
last_sync: %s
cn_version: %s
cnos_commit: %s
hub_name: %s
hub_commit: %s
peer_count: %d
%s
version_drift: %b
```
|} (Cn_fmt.now_iso ()) cn_ver cnos_commit hub_name hub_commit peer_count pkg_line drift in
  Cn_ffi.Fs.write runtime_path content;
  print_endline (Cn_fmt.ok "Updated state/runtime.md")

(* === Status === *)

let run_status hub_path name =
  print_endline (Cn_fmt.info (Printf.sprintf "cn hub: %s" name));
  print_endline "";
  Printf.printf "hub..................... %s\n" (Cn_fmt.green Cn_fmt.check);
  Printf.printf "name.................... %s %s\n" (Cn_fmt.green Cn_fmt.check) name;
  Printf.printf "path.................... %s %s\n" (Cn_fmt.green Cn_fmt.check) hub_path;
  (* Package version display + drift detection *)
  let installed = Cn_assets.list_installed_packages hub_path in
  let has_drift = ref false in
  installed |> List.iter (fun (_pkg_name, pkg_path) ->
    let dir_name = Filename.basename pkg_path in
    match String.index_opt dir_name '@' with
    | Some i ->
      let n = String.sub dir_name 0 i in
      let v = String.sub dir_name (i + 1) (String.length dir_name - i - 1) in
      let ok = v = version in
      if not ok then has_drift := true;
      let status = if ok then Cn_fmt.green (Cn_fmt.check ^ " " ^ v)
        else Cn_fmt.red (Printf.sprintf "%s %s (expected %s)" Cn_fmt.cross v version) in
      Printf.printf "%s................. %s\n" n status
    | None -> ());
  print_endline "";
  if !has_drift then
    print_endline (Cn_fmt.warn (Printf.sprintf "[status] version_drift version=%s" version))
  else
    print_endline (Cn_fmt.dim (Printf.sprintf "[status] ok version=%s" version))

(* === Doctor === *)

type check_result = { name: string; passed: bool; value: string }

let run_doctor hub_path =
  Printf.printf "cn v%s\n" version;
  print_endline (Cn_fmt.info "Checking health...");
  print_endline "";

  let checks = [
    (match Cn_ffi.Child_process.exec "git --version" with
     | Some v ->
         let trimmed = String.trim v in
         let value = match strip_prefix ~prefix:"git version " trimmed with
           | Some rest -> rest | None -> trimmed in
         { name = "git"; passed = true; value }
     | None -> { name = "git"; passed = false; value = "not installed" });

    (match Cn_ffi.Child_process.exec "git config user.name" with
     | Some v -> { name = "git user.name"; passed = true; value = String.trim v }
     | None -> { name = "git user.name"; passed = false; value = "not set" });

    (match Cn_ffi.Child_process.exec "git config user.email" with
     | Some v -> { name = "git user.email"; passed = true; value = String.trim v }
     | None -> { name = "git user.email"; passed = false; value = "not set" });

    (match Cn_ffi.Child_process.exec "patch --version" with
     | Some v ->
         let line = match String.split_on_char '\n' (String.trim v) with
           | first :: _ -> first | [] -> v in
         { name = "patch"; passed = true; value = String.trim line }
     | None -> { name = "patch"; passed = false;
         value = "not installed (required for fs_patch)" });

    (match Cn_ffi.Child_process.exec "curl --version" with
     | Some v ->
         let line = match String.split_on_char '\n' (String.trim v) with
           | first :: _ -> first | [] -> v in
         let value = match String.split_on_char ' ' line with
           | _ :: ver :: _ -> ver | _ -> String.trim v in
         { name = "curl"; passed = true; value }
     | None -> { name = "curl"; passed = false; value = "not installed (required for agent runtime)" });

    { name = "hub directory"; passed = Cn_ffi.Fs.exists hub_path;
      value = if Cn_ffi.Fs.exists hub_path then "exists" else "not found" };

    (let json_path = Cn_ffi.Path.join hub_path ".cn/config.json" in
     let yaml_path = Cn_ffi.Path.join hub_path ".cn/config.yaml" in
     let has_json = Cn_ffi.Fs.exists json_path in
     let has_yaml = Cn_ffi.Fs.exists yaml_path in
     { name = ".cn/config"; passed = has_json || has_yaml;
       value = if has_json then "config.json"
               else if has_yaml then "config.yaml"
               else "missing" });

    (let p = Cn_ffi.Path.join hub_path "spec/SOUL.md" in
     { name = "spec/SOUL.md"; passed = Cn_ffi.Fs.exists p;
       value = if Cn_ffi.Fs.exists p then "exists" else "missing (optional)" });

    (let p = Cn_ffi.Path.join hub_path "state/peers.md" in
     if Cn_ffi.Fs.exists p then
       let count = Cn_ffi.Fs.read p |> String.split_on_char '\n'
         |> List.filter (fun l -> starts_with ~prefix:"- name:" (String.trim l))
         |> List.length in
       { name = "state/peers.md"; passed = true; value = Printf.sprintf "%d peer(s)" count }
     else { name = "state/peers.md"; passed = false; value = "missing" });

    (match Cn_ffi.Child_process.exec_in ~cwd:hub_path "git remote get-url origin" with
     | Some _ -> { name = "origin remote"; passed = true; value = "configured" }
     | None -> { name = "origin remote"; passed = false; value = "not configured" });

    (* v3.5: Package-based cognitive asset checks *)
    (match Cn_assets.validate_packages ~hub_path with
     | Ok () -> { name = "cnos.core doctrine"; passed = true; value = "present" }
     | Error _ -> { name = "cnos.core doctrine"; passed = false;
         value = "missing (run 'cn setup' or 'cn deps restore')" });

    { name = ".cn/deps.json"; passed = Cn_ffi.Fs.exists (Cn_ffi.Path.join hub_path ".cn/deps.json");
      value = if Cn_ffi.Fs.exists (Cn_ffi.Path.join hub_path ".cn/deps.json")
              then "present" else "missing (run 'cn setup')" };

    { name = ".cn/deps.lock.json";
      passed = Cn_ffi.Fs.exists (Cn_ffi.Path.join hub_path ".cn/deps.lock.json");
      value = if Cn_ffi.Fs.exists (Cn_ffi.Path.join hub_path ".cn/deps.lock.json")
              then "present" else "missing (run 'cn setup')" };

    (* v3.19: Deep package system validation — desired/resolved/installed chain *)
    (match Cn_deps.doctor ~hub_path with
     | Ok () -> { name = "package system"; passed = true;
         value = "consistent (manifest ↔ lockfile ↔ installed)" }
     | Error issues ->
         let first = match issues with h :: _ -> h | [] -> "unknown" in
         { name = "package system"; passed = false;
           value = Printf.sprintf "%d issue(s): %s"
             (List.length issues) first });

    (* Runtime contract validation — issue #56, #62 (v2 vertical self-model) *)
    { name = "agent/";
      passed = Cn_ffi.Fs.exists (Cn_ffi.Path.join hub_path "agent");
      value = if Cn_ffi.Fs.exists (Cn_ffi.Path.join hub_path "agent")
              then "exists" else "missing (run 'cn setup')" };

    (let contract_path = Cn_ffi.Path.join hub_path "state/runtime-contract.json" in
     if Cn_ffi.Fs.exists contract_path then begin
       let content = Cn_ffi.Fs.read contract_path in
       match Cn_json.parse content with
       | Ok json ->
         (* v2: four layers — identity, cognition, body, medium *)
         let has_identity = Cn_json.get "identity" json <> None in
         let has_cognition = Cn_json.get "cognition" json <> None in
         let has_body = Cn_json.get "body" json <> None in
         let has_medium = Cn_json.get "medium" json <> None in
         let all_ok = has_identity && has_cognition && has_body && has_medium in
         { name = "runtime contract";
           passed = all_ok;
           value = if all_ok then "valid (identity + cognition + body + medium)"
                   else "incomplete" }
       | Error _ ->
         { name = "runtime contract"; passed = false; value = "invalid JSON" }
     end else
       { name = "runtime contract"; passed = false;
         value = "missing (will be created on next wake)" });

    (* v3.15: Binary/package version coherence *)
    (let installed = Cn_assets.list_installed_packages hub_path in
     let mismatched = installed |> List.filter_map (fun (_name, pkg_path) ->
       let dir_name = Filename.basename pkg_path in
       match String.index_opt dir_name '@' with
       | Some i ->
         let v = String.sub dir_name (i + 1) (String.length dir_name - i - 1) in
         if v <> version then Some (dir_name ^ " (expected " ^ version ^ ")") else None
       | None -> None) in
     if mismatched = [] then
       { name = "version coherence"; passed = true;
         value = Printf.sprintf "binary=%s packages=match" version }
     else
       { name = "version coherence"; passed = false;
         value = Printf.sprintf "drift: %s" (String.concat ", " mismatched) });

    (* v3.17: Extension health — discover, check compatibility, host health *)
    (let reg = Cn_extension.build_registry ~hub_path
       ~runtime_version:version () in
     let entries = Cn_extension.all_entries reg in
     let total = List.length entries in
     let enabled_entries = entries |> List.filter (fun e ->
       e.Cn_extension.state = Cn_extension.Enabled) in
     let enabled = List.length enabled_entries in
     let rejected = entries |> List.filter (fun e ->
       match e.Cn_extension.state with
       | Cn_extension.Rejected _ -> true | _ -> false) |> List.length in
     let disabled = entries |> List.filter (fun e ->
       e.Cn_extension.state = Cn_extension.Disabled) |> List.length in
     (* Check host health for enabled extensions *)
     let unhealthy = enabled_entries |> List.filter_map (fun e ->
       match Cn_extension.resolve_command e with
       | Error reason -> Some (Printf.sprintf "%s: %s" e.manifest.name reason)
       | Ok cmd ->
         match Cn_ext_host.check_health ~command:cmd () with
         | Ok () -> None
         | Error msg -> Some (Printf.sprintf "%s: %s" e.manifest.name msg)
     ) in
     if total = 0 then
       { name = "extensions"; passed = true; value = "none installed" }
     else if rejected > 0 then
       { name = "extensions"; passed = false;
         value = Printf.sprintf "%d installed, %d enabled, %d rejected, %d disabled"
           total enabled rejected disabled }
     else if unhealthy <> [] then
       { name = "extensions"; passed = false;
         value = Printf.sprintf "%d installed, %d enabled (%d unhealthy: %s)"
           total enabled (List.length unhealthy) (String.concat "; " unhealthy) }
     else
       { name = "extensions"; passed = true;
         value = Printf.sprintf "%d installed, %d enabled, %d disabled"
           total enabled disabled });
  ] in

  let width = 22 in
  checks |> List.iter (fun c ->
    let dots = String.make (max 1 (width - String.length c.name)) '.' in
    let status = if c.passed then Cn_fmt.green (Cn_fmt.check ^ " " ^ c.value) else Cn_fmt.red (Cn_fmt.cross ^ " " ^ c.value) in
    Printf.printf "%s%s %s\n" c.name dots status);

  print_endline "";
  let fails = checks |> List.filter (fun c -> not c.passed) |> List.length in
  let oks = List.length checks - fails in
  (if fails = 0 then print_endline (Cn_fmt.ok "All critical checks passed.")
   else print_endline (Cn_fmt.fail (Printf.sprintf "%d issue(s) found." fails)));
  print_endline (Cn_fmt.dim (Printf.sprintf "[status] ok=%d warn=0 fail=%d version=%s" oks fails version))

(* === Sync === *)

(** cn sync — intentionally narrower than maintain_once: runs only the
    transport primitives (inbox check, peer sync with heartbeat, outbox flush)
    plus a runtime projection update.  Omits review, update-check, cleanup,
    and inbox materialization so that `cn sync` stays a quick user-invokable
    transport round-trip rather than a full maintenance tick.
    Delegates to Cn_maintenance primitives to avoid divergence. *)
let run_sync hub_path name =
  print_endline (Cn_fmt.info "Syncing...");
  let _ = Cn_maintenance.inbox_check_once ~hub_path ~name in
  let _ = Cn_maintenance.sync_once ~hub_path in
  let _ = Cn_maintenance.flush_outbox_once ~hub_path ~name in
  update_runtime hub_path;
  print_endline (Cn_fmt.ok "Sync complete")

(* === Setup === *)

(** Read role from .cn/config.json runtime.role, default "engineer". *)
let read_role hub_path =
  let cfg = Cn_ffi.Path.join hub_path ".cn/config.json" in
  if not (Cn_ffi.Fs.exists cfg) then "engineer"
  else
    match Cn_ffi.Fs.read cfg |> Cn_json.parse with
    | Ok json ->
        (match Cn_json.get "runtime" json with
         | Some rt -> Cn_json.get_string "role" rt
           |> Option.map String.lowercase_ascii
           |> Option.value ~default:"engineer"
         | None -> "engineer")
    | Error _ -> "engineer"

(** Read a template file from the installed cnos.core package.
    Returns Ok content if found, Error reason if not.
    Templates are distributed via the "templates" source category
    and installed to .cn/vendor/packages/cnos.core@<ver>/templates/. *)
let read_template ~hub_path template_name =
  match Cn_assets.find_installed_package hub_path "cnos.core" with
  | None -> Error "cnos.core not installed"
  | Some core_path ->
      let path = Cn_ffi.Path.join
        (Cn_ffi.Path.join core_path "templates") template_name in
      if not (Cn_ffi.Fs.exists path) then
        Error (Printf.sprintf "template not found: %s" path)
      else
        Ok (Cn_ffi.Fs.read path)

(** Install cognitive packages into a hub: write deps manifest + lockfile,
    then restore packages. Called by both run_setup and run_init.
    No vendor/core — everything is a package. *)
let setup_assets hub_path =
  print_endline (Cn_fmt.info "Installing cognitive packages...");

  (* Write default manifest if missing *)
  let manifest =
    if Cn_ffi.Fs.exists (Cn_ffi.Path.join hub_path ".cn/deps.json") then
      (match Cn_deps.read_manifest ~hub_path with
       | Some m -> m
       | None ->
           let role = read_role hub_path in
           let m = Cn_deps.default_manifest_for_profile role in
           Cn_deps.write_manifest ~hub_path m;
           print_endline (Cn_fmt.ok (Printf.sprintf "Created .cn/deps.json (profile: %s)" role));
           m)
    else begin
      let role = read_role hub_path in
      let m = Cn_deps.default_manifest_for_profile role in
      Cn_deps.write_manifest ~hub_path m;
      print_endline (Cn_fmt.ok (Printf.sprintf "Created .cn/deps.json (profile: %s)" role));
      m
    end
  in

  (* Write lockfile from manifest (resolves name+version to sha256
     via the package index). *)
  if not (Cn_ffi.Fs.exists (Cn_ffi.Path.join hub_path ".cn/deps.lock.json")) then begin
    (match Cn_deps.lockfile_for_manifest manifest with
     | Ok lock ->
         Cn_deps.write_lockfile ~hub_path lock;
         print_endline (Cn_fmt.ok "Created .cn/deps.lock.json")
     | Error msg ->
         print_endline (Cn_fmt.fail msg);
         Cn_ffi.Process.exit 1)
  end;

  (* Restore from lockfile — MUST succeed for hub to be wake-ready *)
  (match Cn_deps.restore ~hub_path with
   | Ok () ->
       let summary = Cn_assets.summarize ~hub_path in
       let pkg_count = List.length summary.packages in
       print_endline (Cn_fmt.ok (Printf.sprintf
         "Assets ready: %d packages, %d doctrine, %d mindsets"
         pkg_count summary.doctrine_count summary.mindset_count))
   | Error msg ->
       print_endline (Cn_fmt.fail (Printf.sprintf "Restore failed: %s" msg));
       print_endline (Cn_fmt.fail "Hub is NOT wake-ready. Fix the issue and re-run 'cn setup'.");
       Cn_ffi.Process.exit 1)

(** cn setup — interactive hub setup.
    Makes any hub wake-ready: materializes core assets, writes default
    deps manifest with profile package, runs restore.
    Does NOT require root. System-level setup (logrotate, systemd) is
    handled separately. *)
let run_setup hub_path =
  print_endline (Cn_fmt.info (Printf.sprintf "Setting up hub: %s" hub_path));

  (* Ensure required directories exist *)
  Cn_ffi.Fs.ensure_dir (Cn_ffi.Path.join hub_path ".cn");
  Cn_ffi.Fs.ensure_dir (Cn_ffi.Path.join hub_path "agent");

  (* Materialize cognitive substrate *)
  setup_assets hub_path;

  (* Populate spec/SOUL.md and spec/USER.md from templates if missing.
     Ensure spec/ exists -- partial hubs may lack it. *)
  let spec_dir = Cn_ffi.Path.join hub_path "spec" in
  Cn_ffi.Fs.ensure_dir spec_dir;
  begin
    let soul_path = Cn_ffi.Path.join spec_dir "SOUL.md" in
    if not (Cn_ffi.Fs.exists soul_path) then
      (match read_template ~hub_path "SOUL.md" with
       | Ok content ->
           Cn_ffi.Fs.write soul_path content;
           print_endline (Cn_fmt.ok "Created spec/SOUL.md from cnos.core template")
       | Error _ -> ());
    let user_path = Cn_ffi.Path.join spec_dir "USER.md" in
    if not (Cn_ffi.Fs.exists user_path) then
      (match read_template ~hub_path "USER.md" with
       | Ok content ->
           Cn_ffi.Fs.write user_path content;
           print_endline (Cn_fmt.ok "Created spec/USER.md from cnos.core template")
       | Error _ -> ())
  end;

  (* Add .cn/vendor/ to .gitignore if not present *)
  let gitignore_path = Cn_ffi.Path.join hub_path ".gitignore" in
  let existing_gi = if Cn_ffi.Fs.exists gitignore_path
    then Cn_ffi.Fs.read gitignore_path else "" in
  if not (Cn_lib.starts_with ~prefix:".cn/vendor" (String.trim existing_gi))
     && not (Cn_assets.contains_sub existing_gi ".cn/vendor") then begin
    let content = existing_gi ^
      (if existing_gi <> "" && not (Cn_lib.ends_with ~suffix:"\n" existing_gi)
       then "\n" else "") ^ ".cn/vendor/\n" in
    Cn_ffi.Fs.write gitignore_path content;
    print_endline (Cn_fmt.ok "Added .cn/vendor/ to .gitignore")
  end;

  print_endline "";
  print_endline (Cn_fmt.ok "Hub setup complete!")

(* === Update === *)

(** Reconcile first-party packages after a binary update.
    Rewrites lock entries to match the new runtime version, then restores. *)
let reconcile_packages hub_path =
  print_endline (Cn_fmt.info "Reconciling packages with new runtime...");
  let manifest = match Cn_deps.read_manifest ~hub_path with
    | Some m -> m
    | None ->
      let role = read_role hub_path in
      Cn_deps.default_manifest_for_profile role
  in
  (match Cn_deps.lockfile_for_manifest manifest with
   | Error msg ->
       print_endline (Cn_fmt.fail msg);
       Cn_ffi.Process.exit 1
   | Ok lock ->
       Cn_deps.write_lockfile ~hub_path lock);
  (match Cn_deps.restore ~hub_path with
   | Ok () ->
     print_endline (Cn_fmt.ok (Printf.sprintf "Packages reconciled to %s" version));
     update_runtime hub_path
   | Error msg ->
     print_endline (Cn_fmt.fail (Printf.sprintf "Package reconciliation failed: %s" msg)))

let run_update hub_path_opt =
  (* If re-exec'd after binary update, reconcile packages with new version and return *)
  (match Sys.getenv_opt "CN_RECONCILE_HUB" with
   | Some hp when hp <> "" ->
     reconcile_packages hp;
     (* Clear env var so downstream code doesn't see it *)
     Unix.putenv "CN_RECONCILE_HUB" ""
   | _ -> ());

  print_endline (Cn_fmt.info "Checking for updates...");
  print_endline (Printf.sprintf "Current version: %s" version);

  (* One update path: pre-built binaries from GitHub Releases.
     Mirrors install.sh — no git clone, no OCaml toolchain required.
     #37: detects same-version patches via commit hash comparison. *)
  match Cn_agent.get_latest_release () with
  | None ->
      print_endline (Cn_fmt.fail "Could not check for updates (GitHub API unreachable)");
      Cn_ffi.Process.exit 1
  | Some rel ->
      let update_info =
        if Cn_lib.is_newer_version rel.tag version then
          Some (Cn_agent.Update_available rel.tag,
                Printf.sprintf "New version available: %s" rel.tag)
        else if rel.commit <> "" && rel.commit <> Cn_lib.cnos_commit then
          Some (Cn_agent.Update_patch rel.tag,
                Printf.sprintf "Patch available: %s (commit %s → %s)"
                  rel.tag Cn_lib.cnos_commit rel.commit)
        else None
      in
      (match update_info with
      | None ->
          print_endline (Cn_fmt.ok (Printf.sprintf "Already up to date (%s %s)"
            version Cn_lib.cnos_commit))
      | Some (info, msg) ->
        print_endline (Cn_fmt.info msg);
        print_endline (Cn_fmt.info "Downloading pre-built binary...");
        let result = Cn_agent.do_update info in
        (match result with
        | Cn_protocol.Update_complete ->
            print_endline (Cn_fmt.ok (Printf.sprintf "Updated to %s" rel.tag));
            (* Re-exec into the new binary so reconciliation uses the correct
               version/commit constants. The new process detects CN_RECONCILE_HUB
               and reconciles before proceeding with its normal update check. *)
            (match hub_path_opt with
             | Some hp ->
               Unix.putenv "CN_RECONCILE_HUB" hp;
               Cn_agent.re_exec ()
             | None -> ())
        | _ ->
            print_endline (Cn_fmt.fail "Binary download failed. Check network and try again.")))

let run_update_in_hub hub_path =
  run_update (Some hub_path);
  update_runtime hub_path;
  print_endline (Cn_fmt.info "Auto-committing runtime changes...");
  let _ = Cn_ffi.Child_process.exec_in ~cwd:hub_path "git add state/runtime.md" in
  let commit_result = Cn_ffi.Child_process.exec_in ~cwd:hub_path "git commit -m 'chore: cn update runtime'" in
  match commit_result with
  | Some _ ->
      print_endline (Cn_fmt.ok "Committed runtime changes");
      (match Cn_ffi.Child_process.exec_in ~cwd:hub_path "git remote get-url origin 2>/dev/null" with
       | Some _ ->
           print_endline (Cn_fmt.info "Pushing to origin...");
           (match Cn_ffi.Child_process.exec_in ~cwd:hub_path "git push origin HEAD 2>&1" with
            | Some _ -> print_endline (Cn_fmt.ok "Pushed to origin")
            | None -> print_endline (Cn_fmt.warn "Push failed - push manually"))
       | None -> print_endline (Cn_fmt.dim "No origin remote - skipping push"))
  | None -> print_endline (Cn_fmt.dim "No runtime changes to commit")

(* === Self-Update Check === *)

(* Guard re-entry: CN_UPDATE_RUNNING prevents infinite re-exec loop.
   See cn_agent.ml re_exec and MCA: self-update-recursion. *)
let self_update_check () =
  match Sys.getenv_opt "CN_UPDATE_RUNNING" with
  | Some _ -> ()
  | None ->
  if not (Cn_agent.auto_update_enabled ()) then ()
  else
  let args = Cn_ffi.Process.argv |> Array.to_list in
  let is_skip_cmd = List.exists (fun a ->
    a = "--help" || a = "-h" || a = "--version" || a = "-V" || a = "help"
  ) args in
  if is_skip_cmd then ()
  else
    match Cn_agent.get_latest_release () with
    | None -> ()
    | Some rel ->
        let info =
          if Cn_lib.is_newer_version rel.tag version then
            Some (Cn_agent.Update_available rel.tag,
                  Printf.sprintf "Updating cn %s → %s..." version rel.tag)
          else if rel.commit <> "" && rel.commit <> Cn_lib.cnos_commit then
            Some (Cn_agent.Update_patch rel.tag,
                  Printf.sprintf "Patch available: %s (%s → %s)"
                    rel.tag Cn_lib.cnos_commit rel.commit)
          else None
        in
        match info with
        | None -> ()
        | Some (update_info, msg) ->
            print_endline (Cn_fmt.info msg);
            let result = Cn_agent.do_update update_info in
            match result with
            | Cn_protocol.Update_complete ->
                print_endline (Cn_fmt.ok (Printf.sprintf "Updated to cn %s (%s)"
                  rel.tag rel.commit));
                Cn_agent.re_exec ()
            | _ ->
                print_endline (Cn_fmt.warn "Self-update failed - continuing with current version")

(* === Release === *)
(* Removed: run_release. Use scripts/release.sh <version> instead.
   The release workflow (.github/workflows/release.yml) handles GH release
   creation with RELEASE.md notes, binary artifacts, and checksums. *)

(* === Init === *)

let run_init name =
  let hub_name = match name with
    | Some n -> n
    | None -> Cn_ffi.Path.basename (Cn_ffi.Process.cwd ())
  in
  let hub_dir = "cn-" ^ hub_name in

  if Cn_ffi.Fs.exists hub_dir then begin
    print_endline (Cn_fmt.fail (Printf.sprintf "Directory %s already exists" hub_dir));
    Cn_ffi.Process.exit 1
  end;

  print_endline (Cn_fmt.info (Printf.sprintf "Initializing hub: %s" hub_name));

  Cn_ffi.Fs.mkdir_p hub_dir;
  Cn_ffi.Fs.mkdir_p (Cn_ffi.Path.join hub_dir ".cn");
  Cn_ffi.Fs.mkdir_p (Cn_ffi.Path.join hub_dir "spec");
  Cn_ffi.Fs.mkdir_p (Cn_ffi.Path.join hub_dir "state");
  Cn_ffi.Fs.mkdir_p (Cn_ffi.Path.join hub_dir "threads/in");
  Cn_ffi.Fs.mkdir_p (Cn_ffi.Path.join hub_dir "threads/mail/inbox");
  Cn_ffi.Fs.mkdir_p (Cn_ffi.Path.join hub_dir "threads/mail/outbox");
  Cn_ffi.Fs.mkdir_p (Cn_ffi.Path.join hub_dir "threads/mail/sent");
  Cn_ffi.Fs.mkdir_p (Cn_ffi.Path.join hub_dir "threads/reflections/daily");
  Cn_ffi.Fs.mkdir_p (Cn_ffi.Path.join hub_dir "threads/reflections/weekly");
  Cn_ffi.Fs.mkdir_p (Cn_ffi.Path.join hub_dir "threads/reflections/monthly");
  Cn_ffi.Fs.mkdir_p (Cn_ffi.Path.join hub_dir "threads/adhoc");
  Cn_ffi.Fs.mkdir_p (Cn_ffi.Path.join hub_dir "threads/archived");
  Cn_ffi.Fs.mkdir_p (Cn_ffi.Path.join hub_dir "logs");
  (* agent/ override root for hub-local doctrine/mindset/skill overrides *)
  Cn_ffi.Fs.mkdir_p (Cn_ffi.Path.join hub_dir "agent");

  let config = Printf.sprintf {|{
  "name": "%s",
  "version": "1.0.0",
  "created": "%s"
}
|} hub_name (Cn_fmt.now_iso ()) in
  Cn_ffi.Fs.write (Cn_ffi.Path.join hub_dir ".cn/config.json") config;

  let peers = Printf.sprintf {|# Peers

Agents and repos this hub communicates with.

- name: cn-agent
  hub: https://github.com/usurobor/cn-agent.git
  kind: template
|} in
  Cn_ffi.Fs.write (Cn_ffi.Path.join hub_dir "state/peers.md") peers;

  let _ = Cn_ffi.Child_process.exec_in ~cwd:hub_dir "git init -b main" in

  update_runtime hub_dir;

  (* v3.4: Materialize cognitive assets so hub is wake-ready *)
  setup_assets hub_dir;

  (* Write SOUL.md and USER.md from installed cnos.core templates,
     falling back to inline stubs if package not installed (AC3). *)
  Cn_ffi.Fs.ensure_dir (Cn_ffi.Path.join hub_dir "spec");
  let soul = match read_template ~hub_path:hub_dir "SOUL.md" with
    | Ok content -> content
    | Error _ ->
        Printf.sprintf {|# SOUL.md - Core Contract

*%s is an agent on the Coherent Network.*

## Identity

- **Name:** %s
- **Role:** (define your role)
- **Mode:** Collaborative

## What %s Does

- (define responsibilities)

## Conduct

- Be genuinely helpful
- Be resourceful before asking
- Earn trust through competence
|} hub_name hub_name hub_name
  in
  Cn_ffi.Fs.write (Cn_ffi.Path.join hub_dir "spec/SOUL.md") soul;

  let user = match read_template ~hub_path:hub_dir "USER.md" with
    | Ok content -> content
    | Error _ ->
        {|# USER.md - About Your Human

- **Name:** (your human's name)
- **Timezone:** (timezone)

## Preferences

- **Communication:** (style)
- **Autonomy:** (level)
|}
  in
  Cn_ffi.Fs.write (Cn_ffi.Path.join hub_dir "spec/USER.md") user;

  (* Add .cn/vendor/ to .gitignore *)
  let gitignore_path = Cn_ffi.Path.join hub_dir ".gitignore" in
  Cn_ffi.Fs.write gitignore_path ".cn/vendor/\n";

  let _ = Cn_ffi.Child_process.exec_in ~cwd:hub_dir "git add -A" in
  let _ = Cn_ffi.Child_process.exec_in ~cwd:hub_dir (Printf.sprintf "git commit -m %s" (Filename.quote (Printf.sprintf "Initialize %s hub" hub_name))) in

  print_endline (Cn_fmt.ok (Printf.sprintf "Created hub: %s" hub_dir));
  print_endline (Cn_fmt.info "Next steps:");
  print_endline (Printf.sprintf "  cd %s" hub_dir);
  print_endline "  git remote add origin <your-repo-url>";
  print_endline "  git push -u origin main"
