(*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *)

(* TODO(T132410158) Add a module-level doc comment. *)

open Core

(* These configurations are essential in setting up global states and triggering full type
   checking. *)
module BaseConfiguration = struct
  type t = {
    (* Source file discovery *)
    source_paths: Configuration.SourcePaths.t;
    search_paths: SearchPath.t list;
    excludes: string list;
    checked_directory_allowlist: PyrePath.t list;
    checked_directory_blocklist: PyrePath.t list;
    extensions: Configuration.Extension.t list;
    (* Auxiliary paths *)
    log_path: PyrePath.t;
    global_root: PyrePath.t;
    local_root: PyrePath.t option;
    (* Type checking controls *)
    debug: bool;
    enable_type_comments: bool;
    python_version: Configuration.PythonVersion.t;
    system_platform: string option;
    (* Parallelism controls *)
    parallel: bool;
    number_of_workers: int;
    long_lived_workers: bool option;
    (* Memory controls *)
    shared_memory: Configuration.SharedMemory.t;
    (* Logging controls *)
    remote_logging: Configuration.RemoteLogging.t option;
    profiling_output: string option;
    memory_profiling_output: string option;
    enable_readonly_analysis: bool;
    enable_strict_override_check: bool;
    enable_strict_any_check: bool;
    enable_unawaited_awaitable_analysis: bool;
    include_suppressed_errors: bool;
  }
  [@@deriving sexp, compare, hash]

  let of_yojson json =
    let open Yojson.Safe.Util in
    let open JsonParsing.YojsonUtils in
    (* Parsing logic *)
    try
      let source_paths =
        json
        |> member "source_paths"
        |> Configuration.SourcePaths.of_yojson
        |> Result.ok_or_failwith
      in
      let search_paths =
        json
        |> list_member
             "search_paths"
             ~f:(fun element -> to_string element |> SearchPath.create)
             ~default:[]
      in
      let excludes = json |> string_list_member "excludes" ~default:[] in
      let checked_directory_allowlist =
        json |> path_list_member "checked_directory_allowlist" ~default:[]
      in
      let checked_directory_blocklist =
        json |> path_list_member "checked_directory_blocklist" ~default:[]
      in
      let extensions =
        json
        |> string_list_member "extensions" ~default:[]
        |> List.map ~f:Configuration.Extension.create_extension
      in
      let log_path = json |> path_member "log_path" in
      let global_root = json |> path_member "global_root" in
      let local_root = json |> optional_path_member "local_root" in
      let debug = json |> bool_member "debug" ~default:false in
      let enable_type_comments = json |> bool_member "enable_type_comments" ~default:true in
      let python_version =
        json
        |> member "python_version"
        |> function
        | `Null -> Configuration.PythonVersion.create ()
        | _ as json -> Configuration.PythonVersion.of_yojson json |> Result.ok_or_failwith
      in
      let system_platform = json |> optional_string_member "system_platform" in
      let parallel = json |> bool_member "parallel" ~default:false in
      let number_of_workers = json |> int_member "number_of_workers" ~default:1 in
      let long_lived_workers = json |> optional_bool_member "long_lived_workers" in
      let shared_memory =
        json
        |> member "shared_memory"
        |> function
        | `Null -> Configuration.SharedMemory.default
        | _ as json -> Configuration.SharedMemory.of_yojson json |> Result.ok_or_failwith
      in
      let remote_logging =
        json
        |> member "remote_logging"
        |> function
        | `Null -> None
        | _ as json ->
            Configuration.RemoteLogging.of_yojson json |> Result.ok_or_failwith |> Option.some
      in
      let profiling_output = json |> optional_string_member "profiling_output" in
      let memory_profiling_output = json |> optional_string_member "memory_profiling_output" in
      let enable_readonly_analysis =
        json
        |> bool_member
             "enable_readonly_analysis"
             ~default:Configuration.Analysis.default_enable_readonly_analysis
      in
      let enable_strict_override_check =
        json
        |> bool_member
             "enable_strict_override_check"
             ~default:Configuration.Analysis.default_enable_strict_override_check
      in
      let enable_strict_any_check =
        json
        |> bool_member
             "enable_strict_any_check"
             ~default:Configuration.Analysis.default_enable_strict_any_check
      in
      let enable_unawaited_awaitable_analysis =
        json
        |> bool_member
             "enable_unawaited_awaitable_analysis"
             ~default:Configuration.Analysis.default_enable_unawaited_awaitable_analysis
      in
      let include_suppressed_errors =
        json
        |> bool_member
             "include_suppressed_errors"
             ~default:Configuration.Analysis.default_include_suppressed_errors
      in
      Result.Ok
        {
          source_paths;
          search_paths;
          excludes;
          checked_directory_allowlist;
          checked_directory_blocklist;
          extensions;
          log_path;
          global_root;
          local_root;
          debug;
          enable_type_comments;
          python_version;
          system_platform;
          parallel;
          number_of_workers;
          long_lived_workers;
          shared_memory;
          remote_logging;
          profiling_output;
          memory_profiling_output;
          enable_readonly_analysis;
          enable_strict_override_check;
          enable_strict_any_check;
          enable_unawaited_awaitable_analysis;
          include_suppressed_errors;
        }
    with
    | Type_error (message, _)
    | Undefined (message, _) ->
        Result.Error message
    | other_exception -> Result.Error (Exception.exn_to_string other_exception)
end

(* Read a JSON file from the given path. Return [Result.Ok json] if both file read and JSON parsing
   succeeds, and [Result.Error error_message] otherwise. *)
let read_json file_path =
  try Result.Ok (Yojson.Safe.from_file file_path) with
  | Yojson.Json_error message
  | Sys_error message ->
      Result.Error message
  | other_exception -> Result.Error (Exception.exn_to_string other_exception)


(* [read_and_parse_json ~f file_path] reads a JSON out of [file_path] using {! read_json}, and
   invoke [f] on the result JSON. *)
let read_and_parse_json ~f file_path =
  match read_json file_path with
  | Result.Error message ->
      let message = Format.sprintf "Cannot read JSON file. %s" message in
      Result.Error message
  | Result.Ok json -> (
      match f json with
      | Result.Error message ->
          let message = Format.sprintf "Malformed server specification JSON. %s" message in
          Result.Error message
      | Result.Ok _ as result -> result)


(* A convenient wrapper to set up all relevant global states for a Pyre command. *)
let setup_global_states
    ~global_root
    ~local_root
    ~debug
    ~additional_logging_sections
    ~remote_logging
    ~profiling_output
    ~memory_profiling_output
    ()
  =
  Log.GlobalState.initialize ~debug ~sections:additional_logging_sections;
  let logger, log_identifier =
    match remote_logging with
    | None -> None, ""
    | Some { Configuration.RemoteLogging.logger; identifier } -> Some logger, identifier
  in
  let relative_local_root =
    match local_root with
    | None -> ""
    | Some local_root ->
        PyrePath.get_relative_to_root ~root:global_root ~path:local_root |> Option.value ~default:""
  in
  Statistics.GlobalState.initialize
    ~log_identifier
    ?logger
    ~project_root:(PyrePath.absolute global_root)
    ~project_name:relative_local_root
    ();
  PyreProfiling.GlobalState.initialize ?profiling_output ?memory_profiling_output ()
