package linthost

import (
  "fmt"
  "os"
  "slices"

  publicrule "github.com/samchon/ttsc/packages/lint/rule"
)

// RunGraphNodes prints the artifacts every project rule materialized for this
// project, as JSON.
//
// Like the hints verb this takes no `--uri`: a set of artifacts describes the
// Program, not a document. It loads a Program only when the resolved config
// declares a rule that can publish one, and a caller is expected to cache the
// answer and ask again only when the project's inputs changed.
//
// An empty set is a successful answer. A project with no publishing rule is the
// common case, and a caller must be able to tell it apart from a failure; a
// nonzero exit here would read as "the project is broken".
//
// @evidence contracts/common.md#principled-implementation Active publisher settings gate Program loading and passed-state projection; unusable kinds and duplicate addresses are discarded while missing parent links are cleared before JSON output.
// @evidence contracts/common.md#clear-and-simple-design CLI parsing and serialization surround one shared graph computation usable by resident hosts.
// @evidence contracts/common.md#prohibited-implementation-shortcuts Published artifacts come from checked contributor state rather than fabricated graph nodes or coverage results; empty projects remain valid outcomes.
// @evidence contracts/common.md#meaningful-documentation Native prose explains whole-Program scope, input-sensitive caller caching and empty-set success; paragraphs and tags follow documentation guidance.
// @evidence contracts/portability.md#os-neutral-implementation RunGraphNodes resolves native cwd/config/project paths through shared LSP parsing and acquisition helpers before serializing the graph DTO. Graph addresses and protocol strings are distinct from native source-file identity; the dispatcher constructs no shell text or case-folded filesystem identity. Process standard streams remain the JSON/error channels.
// @evidence contracts/performance.md#efficient-algorithms Flags/config and active-publisher metadata are processed before the conditional Program acquisition and project evaluation. Each passed stateful publisher's projection adds its own work; filtering visits g returned nodes, checks the fixed kind vocabulary and hashes addresses/parents, retaining O(g) map/slice entries plus payload backing. JSON serialization adds all output bytes. The empty-publisher gate avoids Program work but not parsing/config checks; no fixed complete-command cost is claimed.
// @evidence contracts/performance.md#reuse-equivalent-work Acquisition uses dependency-validated config reuse and, when installed by the resident host, its warm Program policy. Project evaluation uses the current Program cycle, with resident updates/request setup owning invalidation; the command adds no separate artifact-response cache. Caller result reuse requires unchanged project/rule inputs and valid published state, not merely equal addresses or a quiet watcher.
// @evidence contracts/performance.md#bound-retention-and-release-resources The acquisition callback drops one-shot checker state on all later returns; a resident acquisition preserves the warm Program under its host owner instead. Graph records and JSON buffers are temporary for this invocation, with nested field backing potentially shared with publisher state. The latest project cycle and config/Program caches can outlive the command under their separate owners; this entry sets no node/payload-byte cap, cache eviction or caller deadline.
func RunGraphNodes(args []string) int {
  opts, ok := parseLSPCommandOptions("graph-nodes", args)
  if !ok {
    return 2
  }
  nodes, code := computeGraphNodes(opts)
  if code != 0 {
    return code
  }
  return writeJSON(nodes)
}

// computeGraphNodes builds the artifact set for one project. Split from
// RunGraphNodes so a resident loop can reuse it over a warm Program, the same
// split computeLSPHints takes.
func computeGraphNodes(opts *lspCommandOptions) ([]publicrule.GraphNode, int) {
  rules, err := acquireRules(opts.pluginsJSON, opts.cwd, opts.tsconfig)
  if err != nil {
    fmt.Fprintln(os.Stderr, err)
    return nil, 2
  }
  engine := NewEngineWithResolver(rules)
  if err := engine.ConfigError(); err != nil {
    fmt.Fprintln(os.Stderr, err)
    return nil, 2
  }
  engine.SetCurrentDirectory(opts.cwd)
  publishes, needsChecker := engine.hasGraphPublisher()
  if !publishes {
    // Nothing the config declared can publish artifacts, so there is no
    // projection to take, so this branch skips Program acquisition. Rule/config
    // resolution above has still run; this is not a zero-cost invocation.
    return []publicrule.GraphNode{}, 0
  }
  prog, parseDiags, closeProgram, err := acquireProgram(opts, needsChecker)
  if closeProgram != nil {
    defer closeProgram()
  }
  if err != nil {
    fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
    return nil, 2
  }
  if prog == nil || len(parseDiags) > 0 {
    // The project does not parse right now. Rules never ran, so there are no
    // artifacts — but these are tsgo's diagnostics to own, and failing here
    // would make a consumer treat a syntax error mid-typing as a broken plugin.
    return []publicrule.GraphNode{}, 0
  }
  return collectProjectGraphNodes(prog.runProjectCycle(engine)), 0
}

// hasGraphPublisher reports whether any declared project rule can publish
// artifacts, and whether serving those rules needs a type checker.
//
// Both answers come from the registration table and the resolved config, so
// they are available before a Program exists — which is the point, and matters
// more here than for hints: the artifacts a rule materializes come from parsing
// documents, not from the type system, so a project that publishes them should
// not pay for a checker no projection reads.
func (e *Engine) hasGraphPublisher() (publishes bool, needsChecker bool) {
  if e == nil {
    return false, false
  }
  for _, name := range allProjectRuleNames() {
    setting := e.projectSettings[name]
    if !setting.Declared || setting.Severity == SeverityOff {
      continue
    }
    adapter, registered := registeredProjectRules[name]
    if !registered {
      continue
    }
    if _, publisher := adapter.inner.(publicrule.GraphRule); !publisher {
      continue
    }
    publishes = true
    if projectRuleNeedsTypeChecker(name) {
      // Every publisher's need is folded in rather than returning early: one
      // rule declining a checker does not spare a sibling that reads one.
      needsChecker = true
    }
  }
  return publishes, needsChecker
}

// collectProjectGraphNodes gathers the artifacts every declared project rule
// published for this Program.
//
// It runs after evaluateProject rather than inside it, because a set of
// artifacts is a projection of finished state. A rule is asked only when it
// passed and published state: a rule that failed materialized a description of
// a Program it just rejected, and indexing that would answer questions from
// facts the rule itself disowns.
func collectProjectGraphNodes(cycle *projectCycle) []publicrule.GraphNode {
  if cycle == nil || cycle.results == nil {
    return nil
  }
  nodes := []publicrule.GraphNode{}
  for _, name := range allProjectRuleNames() {
    result, exists := cycle.results.byName[name]
    if !exists || result.reporter == nil {
      continue
    }
    adapter, registered := registeredProjectRules[name]
    if !registered {
      continue
    }
    provider, ok := adapter.inner.(publicrule.GraphRule)
    if !ok {
      continue
    }
    snapshot := result.reporter.snapshot()
    if snapshot.Status != publicrule.ProjectRulePassed || snapshot.State == nil {
      continue
    }
    nodes = append(nodes, ruleGraphNodes(name, provider, result, snapshot)...)
  }
  return dropUnusableGraphNodes(nodes)
}

// ruleGraphNodes calls one rule's GraphNodes behind a recover barrier.
//
// The barrier matches the metadata-inspection contract: a contributor panicking
// while describing itself loses its contribution rather than the process.
func ruleGraphNodes(
  name string,
  provider publicrule.GraphRule,
  result projectCycleResult,
  snapshot publicrule.ProjectRuleResult,
) (nodes []publicrule.GraphNode) {
  defer func() {
    if recovered := recover(); recovered != nil {
      fmt.Fprintf(
        os.Stderr,
        "@ttsc/lint: project rule %q panicked while publishing graph nodes: %v; dropping its artifacts\n",
        name,
        recovered,
      )
      nodes = nil
    }
  }()
  return provider.GraphNodes(&publicrule.GraphContext{
    Identity: result.identity,
    State:    snapshot.State,
    Severity: publicrule.Severity(result.severity),
    Options:  result.options,
  })
}

// dropUnusableGraphNodes removes what a consumer cannot index, and repairs the
// one thing it can: a parent naming no published node.
//
// A node with no address has no identity to be cited by. A node whose kind is
// not in the published vocabulary is one the consumer cannot rank or contain,
// and guessing would be worse than dropping. A duplicate address is kept once,
// first writer winning, because two nodes under one address make a citation
// ambiguous — the rule's own aliases are how one artifact answers to two names.
//
// A parent that survives none of that is cleared rather than dropping its child:
// the child is still a real artifact and still citable, it simply sits at the
// top of its chain. Fabricating the missing parent is what must not happen.
func dropUnusableGraphNodes(nodes []publicrule.GraphNode) []publicrule.GraphNode {
  kinds := publicrule.GraphNodeKinds()
  seen := make(map[string]struct{}, len(nodes))
  kept := make([]publicrule.GraphNode, 0, len(nodes))
  for _, node := range nodes {
    if node.Address == "" || !slices.Contains(kinds, node.Kind) {
      continue
    }
    if _, exists := seen[node.Address]; exists {
      continue
    }
    seen[node.Address] = struct{}{}
    kept = append(kept, node)
  }
  for index := range kept {
    if kept[index].Parent == "" {
      continue
    }
    if _, exists := seen[kept[index].Parent]; !exists {
      kept[index].Parent = ""
    }
  }
  return kept
}
