///|
pub(all) enum ExportDetailLevel {
  ExportCompact
  ExportNormal
  ExportVerbose
} derive(Eq, Debug)

///|
pub(all) struct ExportOptions {
  detail : ExportDetailLevel
  include_parse_issues : Bool
  include_rule_profiles : Bool
  include_sitemaps : Bool
  include_targets : Bool
  include_notes : Bool
} derive(Eq, Debug)

///|
pub fn ExportDetailLevel::label(self : ExportDetailLevel) -> String {
  match self {
    ExportCompact => "compact"
    ExportNormal => "normal"
    ExportVerbose => "verbose"
  }
}

///|
pub fn default_export_options() -> ExportOptions {
  {
    detail: ExportNormal,
    include_parse_issues: true,
    include_rule_profiles: true,
    include_sitemaps: true,
    include_targets: true,
    include_notes: true,
  }
}

///|
pub fn compact_export_options() -> ExportOptions {
  {
    detail: ExportCompact,
    include_parse_issues: false,
    include_rule_profiles: false,
    include_sitemaps: false,
    include_targets: false,
    include_notes: false,
  }
}

///|
pub fn verbose_export_options() -> ExportOptions {
  {
    detail: ExportVerbose,
    include_parse_issues: true,
    include_rule_profiles: true,
    include_sitemaps: true,
    include_targets: true,
    include_notes: true,
  }
}

///|
pub fn RobotsPolicy::to_json(self : RobotsPolicy) -> String {
  policy_to_json(self, default_export_options())
}

///|
pub fn RobotsPolicy::to_compact_json(self : RobotsPolicy) -> String {
  policy_to_json(self, compact_export_options())
}

///|
pub fn RobotsPolicy::to_json_with_options(
  self : RobotsPolicy,
  options : ExportOptions,
) -> String {
  policy_to_json(self, options)
}

///|
pub fn policy_to_json(policy : RobotsPolicy, options : ExportOptions) -> String {
  let fields : Array[String] = []
  fields.push(json_field("group_count", policy.groups.length().to_string()))
  fields.push(json_field("sitemap_count", policy.sitemaps.length().to_string()))
  fields.push(json_field("issue_count", policy.issues.length().to_string()))
  fields.push(json_string_field("host", option_string_to_json(policy.host)))
  fields.push(json_string_field("detail", json_quote(options.detail.label())))
  fields.push("\"groups\":" + groups_to_json(policy.groups, options))
  if options.include_sitemaps {
    fields.push("\"sitemaps\":" + string_array_to_json(policy.sitemaps))
  }
  if options.include_parse_issues {
    fields.push("\"issues\":" + parse_issues_to_json(policy.issues))
  }
  if options.include_rule_profiles {
    fields.push("\"profile\":" + policy.profile().to_json())
  }
  "{" + fields.join(",") + "}"
}

///|
pub fn AgentGroup::to_json(self : AgentGroup) -> String {
  group_to_json(self, default_export_options())
}

///|
pub fn group_to_json(group : AgentGroup, options : ExportOptions) -> String {
  let fields : Array[String] = []
  fields.push("\"agents\":" + string_array_to_json(group.agents))
  fields.push(json_field("line", group.line.to_string()))
  fields.push(json_field("rule_count", group.rules.length().to_string()))
  fields.push(
    json_string_field("crawl_delay", option_int_to_json(group.crawl_delay)),
  )
  fields.push("\"rules\":" + rules_to_json(group.rules, options))
  if options.include_rule_profiles {
    fields.push("\"summary\":" + group.summary(false).to_json())
  }
  "{" + fields.join(",") + "}"
}

///|
pub fn Rule::to_json(self : Rule) -> String {
  let fields : Array[String] = []
  fields.push(json_bool_field("allow", self.allow))
  fields.push(json_string_field("pattern", json_quote(self.pattern)))
  fields.push(json_field("line", self.line.to_string()))
  "{" + fields.join(",") + "}"
}

///|
pub fn RuleProfile::to_json(self : RuleProfile) -> String {
  let fields : Array[String] = []
  fields.push("\"rule\":" + self.rule.to_json())
  fields.push(json_string_field("kind", json_quote(self.kind.label())))
  fields.push(json_string_field("impact", json_quote(self.impact.label())))
  fields.push(json_string_field("risk", json_quote(self.risk.label())))
  fields.push(
    json_string_field("normalized_pattern", json_quote(self.normalized_pattern)),
  )
  fields.push(json_field("literal_weight", self.literal_weight.to_string()))
  fields.push(json_field("depth", self.depth.to_string()))
  fields.push(json_field("wildcard_count", self.wildcard_count.to_string()))
  fields.push(json_bool_field("has_anchor", self.has_anchor))
  fields.push(json_bool_field("is_root_rule", self.is_root_rule))
  fields.push(json_bool_field("is_directory_rule", self.is_directory_rule))
  fields.push(json_bool_field("is_file_rule", self.is_file_rule))
  fields.push(json_bool_field("is_wildcard_rule", self.is_wildcard_rule))
  fields.push(json_string_field("prefix", json_quote(self.prefix)))
  fields.push(
    json_string_field("extension", option_string_to_json(self.extension)),
  )
  fields.push(json_string_field("explanation", json_quote(self.explanation)))
  fields.push("\"notes\":" + string_array_to_json(self.notes))
  "{" + fields.join(",") + "}"
}

///|
pub fn GroupRuleSummary::to_json(self : GroupRuleSummary) -> String {
  let fields : Array[String] = []
  fields.push("\"agents\":" + string_array_to_json(self.agents))
  fields.push(json_field("group_line", self.group_line.to_string()))
  fields.push(json_field("rule_count", self.rule_count.to_string()))
  fields.push(json_field("allow_count", self.allow_count.to_string()))
  fields.push(json_field("disallow_count", self.disallow_count.to_string()))
  fields.push(
    json_field("wildcard_rule_count", self.wildcard_rule_count.to_string()),
  )
  fields.push(
    json_field("anchored_rule_count", self.anchored_rule_count.to_string()),
  )
  fields.push(json_field("root_block_count", self.root_block_count.to_string()))
  fields.push(json_field("empty_rule_count", self.empty_rule_count.to_string()))
  fields.push(json_field("max_depth", self.max_depth.to_string()))
  fields.push(
    json_field("max_literal_weight", self.max_literal_weight.to_string()),
  )
  fields.push(
    json_string_field("crawl_delay", option_int_to_json(self.crawl_delay)),
  )
  fields.push(json_bool_field("has_sitemap_hint", self.has_sitemap_hint))
  fields.push("\"notes\":" + string_array_to_json(self.notes))
  "{" + fields.join(",") + "}"
}

///|
pub fn PolicyProfile::to_json(self : PolicyProfile) -> String {
  let fields : Array[String] = []
  fields.push(
    "\"group_summaries\":" + group_summaries_to_json(self.group_summaries),
  )
  fields.push(json_field("total_rules", self.total_rules.to_string()))
  fields.push(json_field("total_allows", self.total_allows.to_string()))
  fields.push(json_field("total_disallows", self.total_disallows.to_string()))
  fields.push(json_field("total_wildcards", self.total_wildcards.to_string()))
  fields.push(json_field("total_anchors", self.total_anchors.to_string()))
  fields.push(
    json_field("total_root_blocks", self.total_root_blocks.to_string()),
  )
  fields.push(json_field("highest_depth", self.highest_depth.to_string()))
  fields.push(json_field("highest_weight", self.highest_weight.to_string()))
  fields.push(json_bool_field("has_wildcard_group", self.has_wildcard_group))
  fields.push(json_bool_field("has_specific_group", self.has_specific_group))
  fields.push(json_bool_field("has_sitemap", self.has_sitemap))
  fields.push(json_field("issue_count", self.issue_count.to_string()))
  fields.push("\"notes\":" + string_array_to_json(self.notes))
  "{" + fields.join(",") + "}"
}

///|
pub fn AccessDecision::to_json(self : AccessDecision) -> String {
  let fields : Array[String] = []
  fields.push(json_bool_field("allowed", self.allowed))
  fields.push(json_string_field("user_agent", json_quote(self.user_agent)))
  fields.push(json_string_field("path", json_quote(self.path)))
  fields.push(
    json_string_field("matched_group", json_quote(self.matched_group)),
  )
  fields.push(
    json_string_field("matched_rule", matched_rule_to_json(self.matched_rule)),
  )
  fields.push(json_string_field("reason", json_quote(self.reason)))
  "{" + fields.join(",") + "}"
}

///|
pub fn AuditReport::to_json(self : AuditReport) -> String {
  let fields : Array[String] = []
  fields.push("\"policy\":" + self.policy.to_compact_json())
  fields.push("\"findings\":" + findings_to_json(self.findings))
  "{" + fields.join(",") + "}"
}

///|
pub fn Finding::to_json(self : Finding) -> String {
  let fields : Array[String] = []
  fields.push(json_string_field("code", json_quote(self.code)))
  fields.push(json_string_field("severity", json_quote(self.severity.label())))
  fields.push(json_string_field("message", json_quote(self.message)))
  fields.push(json_string_field("detail", json_quote(self.detail)))
  "{" + fields.join(",") + "}"
}

///|
pub fn PathSnapshot::to_json(self : PathSnapshot) -> String {
  let fields : Array[String] = []
  fields.push(json_string_field("original", json_quote(self.original)))
  fields.push(json_string_field("normalized", json_quote(self.normalized)))
  fields.push(json_string_field("source", json_quote(self.source.label())))
  fields.push(json_string_field("query", option_string_to_json(self.query)))
  fields.push(
    json_string_field("fragment", option_string_to_json(self.fragment)),
  )
  fields.push(json_bool_field("had_query", self.had_query))
  fields.push(json_bool_field("had_fragment", self.had_fragment))
  fields.push(json_bool_field("collapsed_slashes", self.collapsed_slashes))
  fields.push(
    json_bool_field("removed_dot_segments", self.removed_dot_segments),
  )
  fields.push(json_bool_field("root_fallback", self.root_fallback))
  "{" + fields.join(",") + "}"
}

///|
pub fn SitemapEntry::to_json(self : SitemapEntry) -> String {
  let fields : Array[String] = []
  fields.push(json_string_field("raw", json_quote(self.raw)))
  fields.push(json_string_field("scheme", json_quote(self.scheme.label())))
  fields.push(json_string_field("host", json_quote(self.host)))
  fields.push(json_string_field("path", json_quote(self.path)))
  fields.push(
    json_string_field("normalized_path", json_quote(self.normalized_path)),
  )
  fields.push(json_string_field("format", json_quote(self.format.label())))
  fields.push(json_bool_field("secure", self.secure))
  fields.push(json_bool_field("absolute", self.absolute))
  fields.push(json_string_field("query", option_string_to_json(self.query)))
  fields.push(
    json_string_field("fragment", option_string_to_json(self.fragment)),
  )
  fields.push(json_field("depth", self.depth.to_string()))
  fields.push(json_field("issue_count", self.issue_count.to_string()))
  fields.push("\"issues\":" + sitemap_issues_to_json(self.issues))
  "{" + fields.join(",") + "}"
}

///|
pub fn SitemapIssue::to_json(self : SitemapIssue) -> String {
  let fields : Array[String] = []
  fields.push(json_string_field("kind", json_quote(self.kind.label())))
  fields.push(json_string_field("sitemap", json_quote(self.sitemap)))
  fields.push(json_string_field("message", json_quote(self.message)))
  "{" + fields.join(",") + "}"
}

///|
pub fn SitemapSummary::to_json(self : SitemapSummary) -> String {
  let fields : Array[String] = []
  fields.push("\"entries\":" + sitemap_entries_to_json(self.entries))
  fields.push(json_field("total", self.total.to_string()))
  fields.push(json_field("https_count", self.https_count.to_string()))
  fields.push(json_field("http_count", self.http_count.to_string()))
  fields.push(json_field("relative_count", self.relative_count.to_string()))
  fields.push(json_field("xml_count", self.xml_count.to_string()))
  fields.push(json_field("gzip_count", self.gzip_count.to_string()))
  fields.push(json_field("index_count", self.index_count.to_string()))
  fields.push(json_field("issue_count", self.issue_count.to_string()))
  fields.push(json_field("duplicate_count", self.duplicate_count.to_string()))
  fields.push("\"unique_hosts\":" + string_array_to_json(self.unique_hosts))
  fields.push("\"notes\":" + string_array_to_json(self.notes))
  "{" + fields.join(",") + "}"
}

///|
pub fn TargetAuditResult::to_json(self : TargetAuditResult) -> String {
  let fields : Array[String] = []
  fields.push("\"target\":" + self.target.to_json())
  fields.push("\"decision\":" + self.decision.to_json())
  fields.push("\"snapshot\":" + self.snapshot.to_json())
  fields.push(
    json_string_field(
      "expectation_status",
      json_quote(self.expectation_status.label()),
    ),
  )
  fields.push(json_bool_field("matched_expected", self.matched_expected))
  fields.push(json_field("importance_score", self.importance_score.to_string()))
  fields.push(json_field("risk_points", self.risk_points.to_string()))
  fields.push("\"tags\":" + string_array_to_json(self.tags))
  "{" + fields.join(",") + "}"
}

///|
pub fn AuditTarget::to_json(self : AuditTarget) -> String {
  let fields : Array[String] = []
  fields.push(json_string_field("label", json_quote(self.label)))
  fields.push(json_string_field("user_agent", json_quote(self.user_agent)))
  fields.push(json_string_field("url_or_path", json_quote(self.url_or_path)))
  fields.push(
    json_string_field(
      "expected_allowed",
      option_bool_to_json(self.expected_allowed),
    ),
  )
  fields.push(
    json_string_field("importance", json_quote(self.importance.label())),
  )
  fields.push(json_string_field("note", json_quote(self.note)))
  "{" + fields.join(",") + "}"
}

///|
pub fn SiteAuditSummary::to_json(self : SiteAuditSummary) -> String {
  let fields : Array[String] = []
  fields.push(json_field("total_targets", self.total_targets.to_string()))
  fields.push(json_field("allowed_targets", self.allowed_targets.to_string()))
  fields.push(json_field("blocked_targets", self.blocked_targets.to_string()))
  fields.push(json_field("expected_targets", self.expected_targets.to_string()))
  fields.push(
    json_field("matched_expectations", self.matched_expectations.to_string()),
  )
  fields.push(
    json_field(
      "mismatched_expectations",
      self.mismatched_expectations.to_string(),
    ),
  )
  fields.push(json_field("critical_targets", self.critical_targets.to_string()))
  fields.push(
    json_field("critical_mismatches", self.critical_mismatches.to_string()),
  )
  fields.push(
    json_field("high_risk_targets", self.high_risk_targets.to_string()),
  )
  fields.push(
    json_field("normalized_targets", self.normalized_targets.to_string()),
  )
  fields.push(json_field("query_targets", self.query_targets.to_string()))
  fields.push(json_field("fragment_targets", self.fragment_targets.to_string()))
  fields.push(json_field("unique_agents", self.unique_agents.to_string()))
  fields.push(
    json_string_field("coverage_band", json_quote(self.coverage_band.label())),
  )
  fields.push(json_field("score", self.score.to_string()))
  fields.push("\"notes\":" + string_array_to_json(self.notes))
  "{" + fields.join(",") + "}"
}

///|
pub fn SiteAuditReport::to_json(self : SiteAuditReport) -> String {
  let fields : Array[String] = []
  fields.push("\"summary\":" + self.summary.to_json())
  fields.push("\"policy\":" + self.policy.to_compact_json())
  fields.push("\"rule_profile\":" + self.rule_profile.to_json())
  fields.push("\"results\":" + target_results_to_json(self.results))
  "{" + fields.join(",") + "}"
}

///|
pub fn AgentMatrix::to_json(self : AgentMatrix) -> String {
  let fields : Array[String] = []
  fields.push(json_field("agent_count", self.agent_count.to_string()))
  fields.push(json_field("path_count", self.path_count.to_string()))
  fields.push(json_field("allowed_count", self.allowed_count.to_string()))
  fields.push(json_field("blocked_count", self.blocked_count.to_string()))
  fields.push("\"rows\":" + matrix_rows_to_json(self.rows))
  "{" + fields.join(",") + "}"
}

///|
pub fn AgentMatrixRow::to_json(self : AgentMatrixRow) -> String {
  let fields : Array[String] = []
  fields.push(json_string_field("user_agent", json_quote(self.user_agent)))
  fields.push(json_string_field("path", json_quote(self.path)))
  fields.push(json_bool_field("allowed", self.allowed))
  fields.push(json_string_field("reason", json_quote(self.reason)))
  fields.push(
    json_string_field("matched_group", json_quote(self.matched_group)),
  )
  fields.push(
    json_string_field("matched_pattern", json_quote(self.matched_pattern)),
  )
  "{" + fields.join(",") + "}"
}

///|
pub fn policy_summary_text(policy : RobotsPolicy) -> String {
  let profile = policy.profile()
  let sitemap_summary = policy.sitemap_summary()
  [
    "RoboPolicy Summary",
    "groups: " + policy.groups.length().to_string(),
    "rules: " + profile.total_rules.to_string(),
    "allows: " + profile.total_allows.to_string(),
    "disallows: " + profile.total_disallows.to_string(),
    "sitemaps: " + sitemap_summary.total.to_string(),
    "issues: " + policy.issues.length().to_string(),
  ].join("\n")
}

///|
pub fn decision_trace_text(
  policy : RobotsPolicy,
  user_agent : StringView,
  url_or_path : StringView,
) -> String {
  let snapshot = normalize_target(url_or_path)
  let decision = policy.decide(user_agent, snapshot.normalized)
  let decision_label = if decision.allowed { "ALLOW" } else { "BLOCK" }
  [
    "RoboPolicy Decision Trace",
    "agent: " + user_agent.to_owned(),
    "input: " + url_or_path.to_owned(),
    "normalized: " + snapshot.normalized,
    "decision: " + decision_label,
    "group: " + decision.matched_group,
    "rule: " + matched_rule_to_text(decision.matched_rule),
    "reason: " + decision.reason,
    "path-flags: " + snapshot.debug_flags(),
  ].join("\n")
}

///|
fn groups_to_json(
  groups : Array[AgentGroup],
  options : ExportOptions,
) -> String {
  let items : Array[String] = []
  for group in groups {
    items.push(group_to_json(group, options))
  }
  "[" + items.join(",") + "]"
}

///|
fn rules_to_json(rules : Array[Rule], options : ExportOptions) -> String {
  let items : Array[String] = []
  for rule in rules {
    if options.detail == ExportVerbose {
      items.push(analyze_rule(rule).to_json())
    } else {
      items.push(rule.to_json())
    }
  }
  "[" + items.join(",") + "]"
}

///|
fn parse_issues_to_json(issues : Array[ParseIssue]) -> String {
  let items : Array[String] = []
  for issue in issues {
    items.push(parse_issue_to_json(issue))
  }
  "[" + items.join(",") + "]"
}

///|
fn parse_issue_to_json(issue : ParseIssue) -> String {
  let fields : Array[String] = []
  fields.push(json_string_field("kind", json_quote(issue.kind.label())))
  fields.push(json_field("line", issue.line.to_string()))
  fields.push(json_string_field("directive", json_quote(issue.directive)))
  fields.push(json_string_field("value", json_quote(issue.value)))
  fields.push(json_string_field("message", json_quote(issue.message)))
  "{" + fields.join(",") + "}"
}

///|
fn group_summaries_to_json(summaries : Array[GroupRuleSummary]) -> String {
  let items : Array[String] = []
  for summary in summaries {
    items.push(summary.to_json())
  }
  "[" + items.join(",") + "]"
}

///|
fn findings_to_json(findings : Array[Finding]) -> String {
  let items : Array[String] = []
  for finding in findings {
    items.push(finding.to_json())
  }
  "[" + items.join(",") + "]"
}

///|
fn sitemap_entries_to_json(entries : Array[SitemapEntry]) -> String {
  let items : Array[String] = []
  for entry in entries {
    items.push(entry.to_json())
  }
  "[" + items.join(",") + "]"
}

///|
fn sitemap_issues_to_json(issues : Array[SitemapIssue]) -> String {
  let items : Array[String] = []
  for issue in issues {
    items.push(issue.to_json())
  }
  "[" + items.join(",") + "]"
}

///|
fn target_results_to_json(results : Array[TargetAuditResult]) -> String {
  let items : Array[String] = []
  for result in results {
    items.push(result.to_json())
  }
  "[" + items.join(",") + "]"
}

///|
fn matrix_rows_to_json(rows : Array[AgentMatrixRow]) -> String {
  let items : Array[String] = []
  for row in rows {
    items.push(row.to_json())
  }
  "[" + items.join(",") + "]"
}

///|
fn string_array_to_json(values : Array[String]) -> String {
  let items : Array[String] = []
  for value in values {
    items.push(json_quote(value))
  }
  "[" + items.join(",") + "]"
}

///|
fn matched_rule_to_json(rule : Rule?) -> String {
  match rule {
    Some(r) => r.to_json()
    None => "null"
  }
}

///|
fn matched_rule_to_text(rule : Rule?) -> String {
  match rule {
    Some(r) => r.pattern
    None => "none"
  }
}

///|
fn option_string_to_json(value : String?) -> String {
  match value {
    Some(text) => json_quote(text)
    None => "null"
  }
}

///|
fn option_int_to_json(value : Int?) -> String {
  match value {
    Some(number) => number.to_string()
    None => "null"
  }
}

///|
fn option_bool_to_json(value : Bool?) -> String {
  match value {
    Some(true) => "true"
    Some(false) => "false"
    None => "null"
  }
}

///|
fn json_field(name : String, value : String) -> String {
  json_quote(name) + ":" + value
}

///|
fn json_string_field(name : String, value : String) -> String {
  json_quote(name) + ":" + value
}

///|
fn json_bool_field(name : String, value : Bool) -> String {
  let json_value = if value { "true" } else { "false" }
  json_quote(name) + ":" + json_value
}

///|
fn json_quote(value : String) -> String {
  "\"" + json_escape(value) + "\""
}

///|
fn json_escape(value : String) -> String {
  value
  .replace_all(old="\\", new="\\\\")
  .replace_all(old="\"", new="\\\"")
  .replace_all(old="\n", new="\\n")
  .replace_all(old="\r", new="\\r")
  .replace_all(old="\t", new="\\t")
}