///|
pub(all) struct ProjectPolicy {
  policy : Policy
  markup : MarkupOptions
  consistency : ConsistencyOptions
} derive(Debug)

///|
pub(all) struct ProjectReport {
  reports : Array[Report]
  findings : Array[ProjectFinding]
  exit_code : Int32
} derive(Debug)

///|
pub fn default_project_policy() -> ProjectPolicy {
  {
    policy: default_policy(),
    markup: default_markup_options(),
    consistency: default_consistency_options(),
  }
}

///|
fn quality_from_json(
  base : ProjectPolicy,
  value : Json,
) -> Result[ProjectPolicy, String] {
  let fields = match value {
    Object(fields) => fields
    _ => return Err("quality must be an object")
  }
  let mut result = base
  for section, value in fields {
    let fields = match value {
      Object(fields) => fields
      _ => return Err(section + " must be an object")
    }
    if section == "markup" {
      let mut options = result.markup
      for key, value in fields {
        match key {
          "mode" =>
            match json_string(value) {
              Some(mode) => options = { ..options, mode, }
              None => return Err("markup mode must be a string")
            }
          "max_chars" | "max_nodes" | "max_depth" => {
            let number = match json_integer(value) {
              Some(number) => number
              None => return Err("markup " + key + " must be an integer")
            }
            options = match key {
              "max_chars" => { ..options, max_chars: number, }
              "max_nodes" => { ..options, max_nodes: number, }
              _ => { ..options, max_depth: number, }
            }
          }
          "translatable_attributes" => {
            let items = match value {
              Array(items) => items
              _ => return Err("translatable_attributes must be a string array")
            }
            let names : Array[String] = []
            for item in items {
              match json_string(item) {
                Some(name) => names.push(name)
                None =>
                  return Err("translatable_attributes must contain strings")
              }
            }
            options = { ..options, translatable_attributes: names, }
          }
          _ => return Err("Unknown markup option: " + key)
        }
      }
      let errors = validate_markup_options(options)
      if !errors.is_empty() {
        return Err(join_strings(errors, "; "))
      }
      result = { ..result, markup: options, }
    } else if section == "consistency" {
      let mut options = result.consistency
      for key, value in fields {
        match key {
          "enabled" | "ignore_context" | "inverse" => {
            let flag = match json_bool(value) {
              Some(flag) => flag
              None => return Err("consistency " + key + " must be a boolean")
            }
            options = match key {
              "enabled" => { ..options, enabled: flag, }
              "ignore_context" => { ..options, ignore_context: flag, }
              _ => { ..options, inverse: flag, }
            }
          }
          "max_entries" =>
            match json_integer(value) {
              Some(number) => options = { ..options, max_entries: number, }
              None => return Err("consistency max_entries must be an integer")
            }
          _ => return Err("Unknown consistency option: " + key)
        }
      }
      if options.max_entries < 1 || options.max_entries > 1000000 {
        return Err("consistency max_entries must be in 1..1000000")
      }
      result = { ..result, consistency: options, }
    } else {
      return Err("Unknown quality section: " + section)
    }
  }
  Ok(result)
}

///|
/// Extends the version-1 policy without changing parse_policy or Options.
pub fn parse_project_policy(text : String) -> Result[ProjectPolicy, String] {
  let value = match bounded_json(text, 16) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let fields = match value {
    Object(fields) => fields
    _ => return Err("Configuration must be an object")
  }
  let base : Map[String, Json] = Map([])
  let mut result = default_project_policy()
  for key, value in fields {
    if key == "quality" {
      match quality_from_json(result, value) {
        Ok(next) => result = next
        Err(error) => return Err(error)
      }
    } else {
      base.set(key, value)
    }
  }
  match parse_policy(Json::object(base).stringify()) {
    Ok(policy) => Ok({ ..result, policy, })
    Err(error) => Err(error)
  }
}

///|
fn project_reports(
  raw : Array[Report],
  project : ProjectPolicy,
  templates : Array[Int32],
) -> ProjectReport {
  let catalogs = raw.mapi((i, report) => {
    if templates.contains(i) {
      { file: report.catalog.file, entries: [], diagnostics: [], }
    } else {
      report.catalog
    }
  })
  let found = check_consistency(catalogs, options=project.consistency)
  let by_file : Array[Array[Diagnostic]] = Array::makei(raw.length(), _ => [])
  for finding in found {
    by_file[finding.file_index].push(finding.diagnostic)
  }
  let reports : Array[Report] = []
  let mut exit_code = 0
  for index, report in raw {
    let diagnostics = report.diagnostics.copy()
    for
      diagnostic in check_markup_catalog(report.catalog, options=project.markup) {
      diagnostics.push(diagnostic)
    }
    for diagnostic in by_file[index] {
      diagnostics.push(diagnostic)
    }
    let checked = apply_policy(
      report_with(
        report.catalog,
        diagnostics,
        project.policy.options,
        report.plural_checked_through,
      ),
      project.policy,
    )
    reports.push(checked)
    if checked.exit_code > exit_code {
      exit_code = checked.exit_code
    }
  }
  let findings : Array[ProjectFinding] = []
  for finding in found {
    let code = finding.diagnostic.code
    if configurable_rule(code) && project.policy.disabled.contains(code) {
      continue
    }
    let diagnostic = if configurable_rule(code) &&
      project.policy.overrides.get(code) is Some(severity) {
      { ..finding.diagnostic, severity, }
    } else {
      finding.diagnostic
    }
    findings.push({ ..finding, diagnostic, })
  }
  { reports, findings, exit_code, }
}

///|
/// Validate target catalogs and apply opt-in markup/project checks and policy.
pub fn check_project(
  catalogs : Array[Catalog],
  configuration~ : ProjectPolicy = default_project_policy(),
) -> ProjectReport {
  project_reports(
    catalogs.map(catalog => {
      check_catalog(catalog, options=configuration.policy.options)
    }),
    configuration,
    [],
  )
}