///|
/// One queried path: eligibility precedes source decoding and safety checks.
pub struct PathExplanation {
  path : String
  eligible : Bool
  status : String
  stage : String
  reason : String
  origin : @selection.RuleOrigin?
  finding_count : Int32
  finding_rules : Array[String]
}

///|
/// A bounded query, not a promise that an entire repository fits its quotas.
pub struct SelectionReport {
  paths : Array[PathExplanation]
  security : SecurityStatus
  style : Style
  candidate_count : Int32
  accepted_count : Int32
  read_bytes : Int64
  max_files : Int32
  max_total_size : Int64
}

///|
/// Inspect only requested local paths. Uses the same selection and source safety
/// stages as pack, without tokenizer loading, transforms, templates or Git export.
/// Does not write files or alter any generated context.
pub async fn explain_selection(
  options : Options,
  paths : Array[String],
) -> SelectionReport {
  validate_source_limits(options)
  if options.remote is Some(_) ||
    options.remote_branch is Some(_) ||
    paths.length() > 256 ||
    paths.any(path => @utf8.encode(path).length() > 4096) {
    raise ConfigurationError
  }
  let cwd = @env.current_dir().unwrap_or(".")
  let destination = if options.stdout {
    None
  } else {
    Some(
      @platform.output_path(options.output, cwd) catch {
        err => {
          if @async.is_cancellation_error(err) {
            raise err
          }
          raise ConfigurationError
        }
      },
    )
  }
  let selected = @selection.explain(
    @platform.absolute(options.directory, cwd),
    {
      include_patterns: options.include_patterns,
      ignore: options.ignore,
      use_gitignore: options.use_gitignore,
      use_dot_ignore: options.use_dot_ignore,
      use_default_patterns: options.use_default_patterns,
      output: destination,
    },
    paths,
    file_paths=options.file_paths,
    max_files=options.max_files,
  ) catch {
    @selection.SelectionError::FileLimitExceeded(limit~) =>
      raise InputLimitExceeded(kind="maxFiles", limit=limit.to_int64())
    err => {
      if @async.is_cancellation_error(err) {
        raise err
      }
      raise SelectionError
    }
  }
  let candidates : Map[String, @selection.Candidate] = Map([])
  for candidate in selected.selection.files {
    candidates[candidate.relative] = candidate
  }
  let results : Array[PathExplanation] = []
  let mut accepted = 0
  let mut read_bytes = 0L
  let mut excluded_secrets = false
  let mut since_timer_poll = 0
  for decision in selected.decisions {
    if !decision.eligible {
      results.push({
        path: decision.path,
        eligible: false,
        status: "excluded",
        stage: "selection",
        reason: decision.reason,
        origin: decision.origin,
        finding_count: 0,
        finding_rules: [],
      })
      continue
    }
    if since_timer_poll >= 32 {
      @async.sleep(0)
      since_timer_poll = 0
    } else {
      @async.pause()
      since_timer_poll += 1
    }
    guard candidates.get(decision.path) is Some(candidate) else {
      raise SelectionError
    }
    let screened = read_screened(
      candidate,
      options,
      options.max_total_size - read_bytes,
    )
    read_bytes += screened.read_bytes
    let rules : Array[String] = []
    for finding in screened.findings {
      if !rules.contains(finding.rule) {
        rules.push(finding.rule)
      }
    }
    rules.sort()
    let (status, stage, reason) = match screened.result {
      Loaded(_) => {
        accepted += 1
        ("accepted", "complete", "Accepted")
      }
      Skipped("SecurityFinding") => {
        excluded_secrets = true
        ("excluded", "security", "SecurityFinding")
      }
      Skipped("UnrepresentableText") =>
        ("excluded", "format", "UnrepresentableText")
      Skipped(reason) => ("excluded", "input", reason)
    }
    results.push({
      path: decision.path,
      eligible: true,
      status,
      stage,
      reason,
      origin: decision.origin,
      finding_count: screened.findings.length(),
      finding_rules: rules,
    })
  }
  {
    paths: results,
    security: if !options.security_check {
      Disabled
    } else if excluded_secrets {
      Excluded
    } else {
      Passed
    },
    style: options.style,
    candidate_count: selected.selection.files.length(),
    accepted_count: accepted,
    read_bytes,
    max_files: options.max_files,
    max_total_size: options.max_total_size,
  }
}

///|
/// Machine-safe projection: no file bodies, rule text or absolute rule paths.
pub fn SelectionReport::to_json(self : SelectionReport) -> Json {
  let security = match self.security {
    Passed => "passed"
    Excluded => "excluded"
    Disabled => "disabled"
  }
  let style = match self.style {
    Xml => "xml"
    Markdown => "markdown"
    Json => "json"
    Plain => "plain"
  }
  let mut findings = 0
  for path in self.paths {
    findings += path.finding_count
  }
  let paths : Array[Json] = self.paths.map(path => {
    let origin : Json = match path.origin {
      None => Json::null()
      Some(origin) => {
        let source : Json = match origin.source {
          None => Json::null()
          Some(value) => Json(value)
        }
        let line : Json = match origin.line {
          None => Json::null()
          Some(value) => Json(value)
        }
        let index : Json = match origin.index {
          None => Json::null()
          Some(value) => Json(value)
        }
        { "kind": origin.kind, "source": source, "line": line, "index": index }
      }
    }
    {
      "path": path.path,
      "eligible": path.eligible,
      "status": path.status,
      "stage": path.stage,
      "reason": path.reason,
      "origin": origin,
      "findingCount": path.finding_count,
      "findingRules": path.finding_rules,
    }
  })
  {
    "schemaVersion": 1,
    "operation": "explain_selection",
    "scope": "requested-paths",
    "style": style,
    "security": {
      "status": security,
      "enabled": !(self.security is Disabled),
      "findings": findings,
    },
    "paths": paths,
    "counts": {
      "requestedPaths": self.paths.length(),
      "candidates": self.candidate_count,
      "accepted": self.accepted_count,
      "excluded": self.paths.length() - self.accepted_count,
    },
    "readBytes": self.read_bytes.to_string(),
    "limits": {
      "maxFiles": self.max_files,
      "maxTotalSize": self.max_total_size.to_string(),
    },
  }
}