-- PistClassifyTrace.lean
-- SilverSight-ruleset-first proof-trace classifier executable.
--
-- Authority: this executable is the sole classifier. It reads a raw trace
-- JSON, computes spectral features with Q16_16 arithmetic, and emits the
-- predicted RRC shape and tactic family. Python shims may only format I/O
-- and call this executable.

import Semantics.PIST.Spectral
import Semantics.PIST.Classify
import Lean.Data.Json

open Lean
open Semantics.PIST

namespace PistClassifyTrace

/-- Raw input expected from the Python shim. -/
structure Input where
  name : String
  transition_matrix : Array (Array Int)
  deriving FromJson

/-- Shape string used when no higher-energy regime is detected.
    Matches RRC.Emit.shapeStr for HoldForUnlawfulOrUnderspecifiedShape. -/
def holdShape : String := "HoldForUnlawfulOrUnderspecifiedShape"

/-- Map the library tactic family to the Python-facing string. -/
def tacticFamilyString : Spectral.TacticFamily → String
  | .rewrite       => "rewrite"
  | .normalization => "normalization"
  | .arithmetic    => "arithmetic"
  | .induction     => "induction"
  | .algebraic     => "algebraic"
  | .case_analysis => "case_analysis"
  | .discharge      => "discharge"
  | .reflexivity   => "reflexivity"
  | .unknown       => "unknown"

/-- Build a JSON object from a Q16_16 spectral profile. -/
def spectralProfileJson (p : Spectral.SpectralProfile) : Json :=
  Json.mkObj [
    ("matrix_size",              toJson p.matrix_size.toInt),
    ("rank",                     toJson p.rank.toInt),
    ("spectral_gap",             toJson p.spectral_gap.toInt),
    ("density",                  toJson p.density.toInt),
    ("trace_val",                toJson p.trace_val.toInt),
    ("frobenius_norm",           toJson p.frobenius_norm.toInt),
    ("laplacian_zero_count",     toJson p.laplacian_zero_count.toInt),
    ("adjacency_eigenvalue_max", toJson p.adjacency_eigenvalue_max.toInt),
    ("laplacian_eigenvalue_max", toJson p.laplacian_eigenvalue_max.toInt),
    ("singular_value_max",       toJson p.singular_value_max.toInt)
  ]

/-- Build the classifier output JSON. -/
def outputJson (name : String) (profile : Spectral.SpectralProfile)
    (shape : String) (tactic : String) : Json :=
  Json.mkObj [
    ("theorem_name",        toJson name),
    ("spectral_radius",     toJson profile.adjacency_eigenvalue_max.toInt),
    ("spectral_radius_q16", toJson profile.adjacency_eigenvalue_max.toInt),
    ("predicted_rrc_shape", toJson shape),
    ("tactic_family",       toJson tactic),
    ("spectral_profile",    spectralProfileJson profile)
  ]

end PistClassifyTrace

def main (args : List String) : IO Unit := do
  let inputStr ←
    match args with
    | [] =>
        let stdin ← IO.getStdin
        stdin.readToEnd
    | [path] => IO.FS.readFile path
    | _ => throw (IO.userError "Usage: pist-classify-trace [trace.json]")

  match Json.parse inputStr with
  | .error e =>
      IO.println (Json.compress (Json.mkObj [("error", toJson s!"JSON parse error: {e}")]))
  | .ok j =>
      match (fromJson? j : Except String PistClassifyTrace.Input) with
      | .error e =>
          IO.println (Json.compress (Json.mkObj [("error", toJson e)]))
      | .ok input =>
          let profile := Spectral.computeSpectral input.transition_matrix
          let shape := (Classify.classifyExact input.transition_matrix).getD PistClassifyTrace.holdShape
          let tactic := PistClassifyTrace.tacticFamilyString (Spectral.classifyTacticFromName input.name)
          IO.println (Json.compress (PistClassifyTrace.outputJson input.name profile shape tactic))
