lib/foundry/status.ex

defmodule Foundry.Status do
  @moduledoc """
  Runtime health picture composed from all Phase 1 data.

  Assembles project status from:
  - Compilation state (compiled_at from .beam files)
  - Stack versions (from mix.lock)
  - Lint violations (from Foundry.Lint.Runner)
  - Migrations (pending count from Foundry.Manifest)
  - Proposals (open count from proposal storage)
  - Compliance matrix (from Foundry.Context.ProjectContext)
  - Test coverage (from project fixture)
  - CI state (from lock file and optional .foundry/ci_status.json)
  - Project manifest metadata
  """

  defstruct [
    :generated_at,
    :compiled_at,
    :project,
    :project_type,
    :domain_type,
    :domains,
    :sensitive_modules,
    :lint,
    :migrations,
    :proposals,
    :compliance,
    :test_coverage,
    :ci,
    :stack,
    :manifest
  ]

  def build(project_root) do
    {:ok, manifest} = Foundry.Manifest.Parser.read(project_root)
    {nodes, _edges} = Foundry.Context.GraphBuilder.build(project_root, manifest)

    build_from_nodes(project_root, manifest, nodes)
  end

  def build_from_nodes(project_root, manifest, nodes) do
    domain_type = Keyword.get(manifest, :domain_type, "")
    domain_type_str = if is_atom(domain_type), do: Atom.to_string(domain_type), else: domain_type

    project_name = Keyword.get(manifest, :project_name, "")
    project_type = Keyword.get(manifest, :project_type, "standard")

    # Fetch remaining components
    lint_report = Foundry.Lint.Runner.run(project_root)
    stack_versions = Foundry.Status.StackVersions.read(project_root)
    compiled_at = compiled_at(project_root)

    # Extract domains from nodes
    domains = extract_domains(nodes)

    # Extract sensitive modules from nodes
    sensitive_modules = extract_sensitive_modules(nodes)

    # Build status structure
    %{
      "generated_at" => DateTime.utc_now() |> DateTime.to_iso8601(),
      "compiled_at" => compiled_at,
      "project" => project_name,
      "project_type" => project_type,
      "domain_type" => domain_type_str,
      "domains" => domains,
      "sensitive_modules" => sensitive_modules,
      "lint" => lint_to_status(lint_report),
      "migrations" => migrations_status(manifest),
      "proposals" => proposals_status(project_root),
      "compliance" => compliance_status(nodes),
      "test_coverage" => test_coverage_status(),
      "ci" => ci_status(project_root),
      "stack" => stack_versions,
      "manifest" => manifest_summary(manifest)
    }
  end

  defp compiled_at(project_root) do
    # Try to find .beam files in common build directories
    beam_files =
      [
        # Try looking for any .beam files in dev ebin
        Path.join([project_root, "_build", "dev", "lib", "*", "ebin", "*.beam"]),
        # Try looking for any .beam files in test ebin
        Path.join([project_root, "_build", "test", "lib", "*", "ebin", "*.beam"])
      ]
      |> Enum.flat_map(&Path.wildcard/1)

    case beam_files do
      [] ->
        nil

      beams ->
        beams
        |> Enum.map(&File.stat!(&1).mtime)
        |> Enum.max()
        |> NaiveDateTime.from_erl!()
        |> DateTime.from_naive!("Etc/UTC")
        |> DateTime.to_iso8601()
    end
  end

  defp extract_domains(nodes) do
    nodes
    |> Enum.map(& &1.domain)
    |> Enum.uniq()
    |> Enum.sort()
  end

  defp extract_sensitive_modules(nodes) do
    nodes
    |> Enum.filter(& &1.sensitive)
    |> Enum.map(fn node ->
      # Extract short name from FQN: MyApp.Domain.Resource -> Resource
      node.module
      |> String.split(".")
      |> List.last()
    end)
    |> Enum.uniq()
    |> Enum.sort()
  end

  defp lint_to_status(lint_report) do
    %{
      "errors" => Enum.count(lint_report.violations, &(&1.severity == :error)),
      "warnings" => Enum.count(lint_report.violations, &(&1.severity == :warning)),
      "total_violations" => length(lint_report.violations)
    }
  end

  defp migrations_status(_manifest) do
    # TODO: Implement pending migration count from Foundry.Manifest
    # For now, return placeholder
    %{
      "pending_count" => 0,
      "applied_count" => 0
    }
  end

  defp proposals_status(project_root) do
    # TODO: Implement proposal count from storage
    # For now, return placeholder
    _ = project_root

    %{
      "open_count" => 0,
      "recent" => []
    }
  end

  defp compliance_status(nodes) do
    # Extract compliance requirements from nodes
    # Convert underscore format (RG_MGA_001) to hyphen format (RG-MGA-001)
    requirements =
      nodes
      |> Enum.flat_map(& &1.compliance)
      |> Enum.uniq()
      |> Enum.sort()
      |> Enum.map(fn req_id ->
        # Convert from atom or underscore string to hyphenated string
        id_str =
          req_id
          |> to_string()
          |> String.replace("_", "-")

        %{
          "id" => id_str,
          "status" => "planned",
          "coverage" => 0
        }
      end)

    %{
      "total_requirements" => length(requirements),
      "covered_count" => 0,
      "planned_count" => length(requirements),
      "requirements" => requirements
    }
  end

  defp test_coverage_status do
    # TODO: Implement test coverage detection
    %{
      "unit" => 0,
      "integration" => 0,
      "e2e" => 0
    }
  end

  defp ci_status(project_root) do
    lock_current = match?(:ok, Foundry.Context.LockFile.check(project_root))

    base = %{
      "context_lock_current" => lock_current,
      "last_run_at" => nil,
      "commit" => nil,
      "branch" => nil,
      "lint_passed" => nil,
      "tests_passed" => nil
    }

    # Overlay with CI-written status file if present
    case Foundry.FileSystem.read(project_root, ".foundry/ci_status.json") do
      {:ok, content} ->
        Map.merge(base, Jason.decode!(content))

      _error ->
        base
    end
  end

  defp manifest_summary(manifest) do
    domain_type = Keyword.get(manifest, :domain_type, "")
    domain_type_str = if is_atom(domain_type), do: Atom.to_string(domain_type), else: domain_type

    %{
      "domain_type" => domain_type_str,
      "sensitive_resources" => Keyword.get(manifest, :sensitive_resources, [])
    }
  end
end