open! Dune_exe
open! Dune_exe__Pkg__
open Import
open Pkg_common

let find_outdated_packages ~transitive ~lock_dirs_arg () =
  let open Fiber.O in
  let+ pps, not_founds =
    let* workspace = Memo.run (Workspace.workspace ()) in
    Pkg_common.Lock_dirs_arg.lock_dirs_of_workspace lock_dirs_arg workspace
    |> Fiber.parallel_map ~f:(fun lock_dir_path ->
      let lock_dir_path = Path.source lock_dir_path in
      (* updating makes sense when checking for outdated packages *)
      let* repos =
        Dune_pkg.Opam_repo.resolve_repositories
          ~available_repos:(repositories_of_workspace workspace)
          ~repositories:(repositories_of_lock_dir workspace ~lock_dir_path)
      and+ local_packages = Memo.run find_local_packages
      and+ platform = solver_env_from_system_and_context ~lock_dir_path in
      let lock_dir = Dune_pkg.Lock_dir.read_disk_exn lock_dir_path in
      let packages =
        Dune_pkg.Lock_dir.Packages.pkgs_on_platform_by_name lock_dir.packages ~platform
      in
      let+ results = Dune_pkg.Outdated.find ~repos ~local_packages packages in
      ( Dune_pkg.Outdated.pp ~transitive ~lock_dir_path results
      , ( Dune_pkg.Outdated.packages_that_were_not_found results
          |> Package_name.Set.of_list
          |> Package_name.Set.to_list
        , lock_dir_path
        , repos ) ))
    >>| List.split
  in
  (match pps with
   | [ _ ] -> Console.print pps
   | _ -> Console.print [ Pp.enumerate ~f:Fun.id pps ]);
  let error_messages =
    List.filter_map not_founds ~f:(function
      | [], _, _ -> None
      | packages, lock_dir_path, repos ->
        Pp.concat
          ~sep:Pp.space
          [ Pp.textf
              "When checking %s, the following packages:"
              (Path.to_string_maybe_quoted lock_dir_path)
            |> Pp.hovbox
          ; Pp.concat
              ~sep:Pp.space
              [ Pp.enumerate packages ~f:(fun name ->
                  Dune_lang.Package_name.to_string name |> Pp.verbatim)
              ; Pp.text "were not found in the following opam repositories:" |> Pp.hovbox
              ; Pp.enumerate repos ~f:(fun repo ->
                  (* CR-rgrinberg: why are we outputting [Dyn.t] in error
                     messages? *)
                  Dune_pkg.Opam_repo.serializable repo
                  |> Dyn.option Dune_pkg.Opam_repo.Serializable.to_dyn
                  |> Dyn.pp)
              ]
            |> Pp.vbox
          ]
        |> Pp.hovbox
        |> Option.some)
  in
  match error_messages with
  | [] -> ()
  | error_messages ->
    User_error.raise (Pp.text "Some packages could not be found." :: error_messages)
;;

let term =
  let+ builder = Common.Builder.term
  and+ transitive =
    Arg.(
      value
      & flag
      & info
          [ "transitive" ]
          ~doc:(Some "Check for outdated packages in transitive dependencies"))
  and+ lock_dirs_arg = Pkg_common.Lock_dirs_arg.term in
  let builder = Common.Builder.forbid_builds builder in
  let common, config = Common.init builder in
  Scheduler_setup.go_with_rpc_server ~common ~config (fun () ->
    let open Fiber.O in
    Pkg_common.check_pkg_management_enabled ()
    >>> find_outdated_packages ~transitive ~lock_dirs_arg ())
;;

let info =
  let doc = "Check for outdated packages" in
  let man =
    [ `S "DESCRIPTION"
    ; `P
        "List packages in from lock directory that have newer versions available. By \
         default, only direct dependencies are checked. The $(b,--transitive) flag can \
         be used to check transitive dependencies as well."
    ; `P "For example:"
    ; `Pre "    \\$ dune pkg outdated"
    ; `Noblank
    ; `Pre "    1/2 packages in dune.lock are outdated."
    ; `Noblank
    ; `Pre "    - ocaml 4.14.1 < 5.1.0"
    ; `Noblank
    ; `Pre "    - dune 3.7.1 < 3.11.0"
    ]
  in
  Cmd.info "outdated" ~doc ~man
;;

let command = Cmd.v info term
