///|
/// Profiles express the intended publication target without using network APIs.
pub(all) enum DoctorProfile {
  Default
  Github
  Mooncakes
  Release
} derive(Debug, Eq)

///|
pub fn DoctorProfile::label(self : DoctorProfile) -> String {
  match self {
    Default => "default"
    Github => "github"
    Mooncakes => "mooncakes"
    Release => "release"
  }
}

///|
fn parse_profile(value : String) -> DoctorProfile? {
  match value {
    "default" => Some(Default)
    "github" => Some(Github)
    "mooncakes" => Some(Mooncakes)
    "release" => Some(Release)
    _ => None
  }
}

///|
fn parse_severity(value : String) -> Severity? {
  match value {
    "error" => Some(Error)
    "warn" | "warning" => Some(Warn)
    "info" => Some(Info)
    _ => None
  }
}

///|
/// Project-local policy loaded from moon.doctor.json when present.
pub(all) struct DoctorConfig {
  version : Int
  profile : DoctorProfile
  disabled_rules : Array[String]
  severity_overrides : Map[String, Severity]
  ignore_paths : Array[String]
  required_commands : Array[String]
  release_metadata : Map[String, String]
  source : String
} derive(Debug, Eq)

///|
pub fn DoctorConfig::default(
  profile? : DoctorProfile = Default,
) -> DoctorConfig {
  let required_commands = match profile {
    Default => ["moon check", "moon test"]
    Github => ["moon check", "moon test", "git status --short --branch"]
    Mooncakes => ["moon check", "moon test", "moon package"]
    Release =>
      [
        "moon check --target all --deny-warn", "moon build --target all --deny-warn",
        "moon test --target all --deny-warn", "moon package",
      ]
  }
  {
    version: 1,
    profile,
    disabled_rules: [],
    severity_overrides: {},
    ignore_paths: ["_build", ".mooncakes", ".git", "target"],
    required_commands,
    release_metadata: {},
    source: "builtin:" + profile.label(),
  }
}

///|
pub fn DoctorConfig::is_rule_disabled(
  self : DoctorConfig,
  rule_id : String,
) -> Bool {
  self.disabled_rules.contains(rule_id)
}

///|
pub fn DoctorConfig::severity_for(
  self : DoctorConfig,
  rule_id : String,
  default_severity : Severity,
) -> Severity {
  match self.severity_overrides.get(rule_id) {
    Some(value) => value
    None => default_severity
  }
}

///|
pub fn DoctorConfig::release_checklist(self : DoctorConfig) -> Array[String] {
  let checklist = default_release_checklist()
  for command in self.required_commands {
    let entry = "Run " + command
    if !checklist.contains(entry) {
      checklist.push(entry)
    }
  }
  checklist
}

///|
pub fn DoctorConfig::ignores_path(self : DoctorConfig, path : String) -> Bool {
  self.ignore_paths.any(ignore => {
    path == ignore ||
    path.has_prefix(ignore + "/") ||
    path.has_prefix(ignore + "\\")
  })
}

///|
fn config_path(root : String) -> String {
  if root.has_suffix("/") || root.has_suffix("\\") {
    root + "moon.doctor.json"
  } else {
    root + "/moon.doctor.json"
  }
}

///|
fn string_value(
  object : Map[String, Json],
  key : String,
  fallback : String,
) -> String raise {
  match object.get(key) {
    Some(value) => @json.from_json(value)
    None => fallback
  }
}

///|
fn string_array_value(
  object : Map[String, Json],
  key : String,
) -> Array[String] raise {
  match object.get(key) {
    Some(value) => @json.from_json(value)
    None => []
  }
}

///|
fn string_map_value(
  object : Map[String, Json],
  key : String,
) -> Map[String, String] raise {
  match object.get(key) {
    Some(value) => @json.from_json(value)
    None => {}
  }
}

///|
fn severity_map_value(
  object : Map[String, Json],
) -> Map[String, Severity] raise {
  let raw = string_map_value(object, "severity_overrides")
  let parsed : Map[String, Severity] = {}
  for rule_id, value in raw {
    match parse_severity(value) {
      Some(severity) => parsed[rule_id] = severity
      None =>
        fail(
          "moon.doctor.json has invalid severity for " + rule_id + ": " + value,
        )
    }
  }
  parsed
}

///|
fn validate_known_fields(object : Map[String, Json]) -> Unit raise {
  let known = [
    "version", "profile", "disabled_rules", "severity_overrides", "ignore_paths",
    "required_commands", "release_metadata",
  ]
  for key in object.keys() {
    if !known.contains(key) {
      fail("moon.doctor.json has unknown field: " + key)
    }
  }
}

///|
fn validate_unique(label : String, values : Array[String]) -> Unit raise {
  let seen : Map[String, Unit] = {}
  for value in values {
    if seen.contains(value) {
      fail("moon.doctor.json has duplicate " + label + ": " + value)
    }
    seen[value] = ()
  }
}

///|
fn parse_config(content : String, source : String) -> DoctorConfig raise {
  let json = @json.parse(content)
  let object : Map[String, Json] = @json.from_json(json)
  validate_known_fields(object)
  let version = match object.get("version") {
    Some(value) => {
      let parsed : Int = @json.from_json(value)
      parsed
    }
    None => 1
  }
  if version != 1 {
    fail("moon.doctor.json supports only version 1")
  }
  let profile_name = string_value(object, "profile", "default")
  let profile = match parse_profile(profile_name) {
    Some(value) => value
    None => fail("moon.doctor.json has unknown profile: " + profile_name)
  }
  let defaults = DoctorConfig::default(profile~)
  let disabled_rules = string_array_value(object, "disabled_rules")
  let ignore_paths = match object.get("ignore_paths") {
    Some(_) => string_array_value(object, "ignore_paths")
    None => defaults.ignore_paths
  }
  let required_commands = match object.get("required_commands") {
    Some(_) => string_array_value(object, "required_commands")
    None => defaults.required_commands
  }
  validate_unique("disabled rule", disabled_rules)
  validate_unique("ignored path", ignore_paths)
  validate_unique("required command", required_commands)
  {
    version,
    profile,
    disabled_rules,
    severity_overrides: severity_map_value(object),
    ignore_paths,
    required_commands,
    release_metadata: string_map_value(object, "release_metadata"),
    source,
  }
}

///|
/// Load a project policy or return the selected built-in profile when absent.
pub fn load_config(
  root : String,
  path? : String,
  profile? : String,
) -> DoctorConfig raise {
  let requested_profile = match profile {
    Some(value) =>
      match parse_profile(value) {
        Some(parsed) => parsed
        None => fail("unknown profile: " + value)
      }
    None => Default
  }
  let path = match path {
    Some(value) => value
    None => config_path(root)
  }
  if @fs.path_exists(path) {
    let config = parse_config(@fs.read_file_to_string(path), path)
    match profile {
      Some(_) =>
        DoctorConfig::{
          ..config,
          profile: requested_profile,
          source: path + " + CLI profile",
        }
      None => config
    }
  } else {
    DoctorConfig::default(profile=requested_profile)
  }
}