///|
/// Counted file group used for an explainable project inventory summary.
pub(all) struct MetricGroup {
  name : String
  files : Int
  lines : Int
  characters : Int
} derive(Debug, Eq)

///|
pub fn MetricGroup::new(name : String) -> MetricGroup {
  { name, files: 0, lines: 0, characters: 0 }
}

///|
pub fn MetricGroup::add_file(
  self : MetricGroup,
  file : ProjectFile,
) -> MetricGroup {
  {
    name: self.name,
    files: self.files + 1,
    lines: self.lines + file.line_count,
    characters: self.characters + file.char_count,
  }
}

///|
/// Structural metrics produced entirely from local repository contents.
pub(all) struct ProjectMetrics {
  root : String
  total_files : Int
  total_directories : Int
  total_lines : Int
  total_characters : Int
  moonbit_source : MetricGroup
  moonbit_tests : MetricGroup
  markdown_docs : MetricGroup
  workflows : MetricGroup
  config_files : MetricGroup
  fixture_files : MetricGroup
  example_files : MetricGroup
  package_count : Int
  local_dependency_edges : Int
  ignored_path_count : Int
} derive(Debug, Eq)

///|
fn category_for_file(file : ProjectFile) -> String {
  if file.path.has_suffix("_test.mbt") || file.path.has_suffix("_wbtest.mbt") {
    "moonbit_tests"
  } else if file.path.has_suffix(".mbt") {
    "moonbit_source"
  } else if file.path.has_suffix(".md") {
    "markdown_docs"
  } else if file.path.has_prefix(".github/workflows/") {
    "workflows"
  } else if file.path.has_suffix(".json") ||
    file.path.has_suffix(".yml") ||
    file.path.has_suffix(".yaml") {
    "config_files"
  } else if file.path.has_prefix("fixtures/") {
    "fixture_files"
  } else if file.path.has_prefix("examples/") {
    "example_files"
  } else {
    "other"
  }
}

///|
fn update_group(group : MetricGroup, file : ProjectFile) -> MetricGroup {
  group.add_file(file)
}

///|
pub fn calculate_project_metrics(
  inventory : ProjectInventory,
  graph : PackageGraph,
) -> ProjectMetrics {
  let mut source = MetricGroup::new("MoonBit source")
  let mut tests = MetricGroup::new("MoonBit tests")
  let mut docs = MetricGroup::new("Markdown documentation")
  let mut workflows = MetricGroup::new("GitHub workflows")
  let mut configs = MetricGroup::new("Configuration")
  let mut fixtures = MetricGroup::new("Fixtures")
  let mut examples = MetricGroup::new("Examples")
  let mut characters = 0
  for file in inventory.files {
    characters += file.char_count
    match category_for_file(file) {
      "moonbit_source" => source = update_group(source, file)
      "moonbit_tests" => tests = update_group(tests, file)
      "markdown_docs" => docs = update_group(docs, file)
      "workflows" => workflows = update_group(workflows, file)
      "config_files" => configs = update_group(configs, file)
      "fixture_files" => fixtures = update_group(fixtures, file)
      "example_files" => examples = update_group(examples, file)
      _ => ()
    }
  }
  {
    root: inventory.root,
    total_files: inventory.files.length(),
    total_directories: inventory.directories.length(),
    total_lines: inventory.total_lines(),
    total_characters: characters,
    moonbit_source: source,
    moonbit_tests: tests,
    markdown_docs: docs,
    workflows,
    config_files: configs,
    fixture_files: fixtures,
    example_files: examples,
    package_count: graph.package_count(),
    local_dependency_edges: graph.edge_count(),
    ignored_path_count: inventory.ignored_paths.length(),
  }
}

///|
fn metric_group_to_json(group : MetricGroup) -> Json {
  Json::object({
    "name": Json::string(group.name),
    "files": Json::number(group.files.to_double()),
    "lines": Json::number(group.lines.to_double()),
    "characters": Json::number(group.characters.to_double()),
  })
}

///|
pub fn project_metrics_to_json(metrics : ProjectMetrics) -> Json {
  Json::object({
    "root": Json::string(metrics.root),
    "total_files": Json::number(metrics.total_files.to_double()),
    "total_directories": Json::number(metrics.total_directories.to_double()),
    "total_lines": Json::number(metrics.total_lines.to_double()),
    "total_characters": Json::number(metrics.total_characters.to_double()),
    "moonbit_source": metric_group_to_json(metrics.moonbit_source),
    "moonbit_tests": metric_group_to_json(metrics.moonbit_tests),
    "markdown_docs": metric_group_to_json(metrics.markdown_docs),
    "workflows": metric_group_to_json(metrics.workflows),
    "config_files": metric_group_to_json(metrics.config_files),
    "fixture_files": metric_group_to_json(metrics.fixture_files),
    "example_files": metric_group_to_json(metrics.example_files),
    "package_count": Json::number(metrics.package_count.to_double()),
    "local_dependency_edges": Json::number(
      metrics.local_dependency_edges.to_double(),
    ),
    "ignored_path_count": Json::number(metrics.ignored_path_count.to_double()),
  })
}

///|
fn maturity_signal(name : String, complete : Bool, evidence : String) -> String {
  (if complete { "PASS" } else { "GAP" }) + " " + name + ": " + evidence
}

///|
pub fn render_project_metrics(metrics : ProjectMetrics) -> String {
  let builder = StringBuilder()
  builder.write_string("Moon Doctor project metrics\n")
  builder.write_string("Root: " + metrics.root + "\n")
  builder.write_string(
    "Files: \{metrics.total_files}, directories: \{metrics.total_directories}, lines: \{metrics.total_lines}\n\n",
  )
  for
    group in [
      metrics.moonbit_source,
      metrics.moonbit_tests,
      metrics.markdown_docs,
      metrics.workflows,
      metrics.config_files,
      metrics.fixture_files,
      metrics.example_files,
    ] {
    builder.write_string(
      "- " + group.name + ": \{group.files} files, \{group.lines} lines\n",
    )
  }
  builder.write_string("\nMaturity signals\n")
  builder.write_string(
    maturity_signal(
      "source",
      metrics.moonbit_source.files > 0,
      "\{metrics.moonbit_source.files} source files",
    ) +
    "\n",
  )
  builder.write_string(
    maturity_signal(
      "tests",
      metrics.moonbit_tests.files > 0,
      "\{metrics.moonbit_tests.files} test files",
    ) +
    "\n",
  )
  builder.write_string(
    maturity_signal(
      "documentation",
      metrics.markdown_docs.files > 0,
      "\{metrics.markdown_docs.files} Markdown files",
    ) +
    "\n",
  )
  builder.write_string(
    maturity_signal(
      "CI",
      metrics.workflows.files > 0,
      "\{metrics.workflows.files} workflow files",
    ) +
    "\n",
  )
  builder.write_string(
    maturity_signal(
      "packages",
      metrics.package_count > 0,
      "\{metrics.package_count} packages",
    ) +
    "\n",
  )
  builder.to_string()
}

///|
pub fn metrics_markdown(metrics : ProjectMetrics) -> String {
  let builder = StringBuilder()
  builder.write_string("| Metric | Files | Lines |\n| --- | ---: | ---: |\n")
  for
    group in [
      metrics.moonbit_source,
      metrics.moonbit_tests,
      metrics.markdown_docs,
      metrics.workflows,
      metrics.config_files,
      metrics.fixture_files,
      metrics.example_files,
    ] {
    builder.write_string(
      "| " + group.name + " | \{group.files} | \{group.lines} |\n",
    )
  }
  builder.to_string()
}