///|
pub(all) enum SitemapScheme {
  SitemapHttp
  SitemapHttps
  SitemapRelative
  SitemapUnknown
} derive(Eq, Debug)

///|
pub(all) enum SitemapFormat {
  SitemapXml
  SitemapXmlGzip
  SitemapIndex
  SitemapText
  SitemapUnknownFormat
} derive(Eq, Debug)

///|
pub(all) enum SitemapIssueKind {
  SitemapMissingHost
  SitemapInsecureHttp
  SitemapUnsupportedScheme
  SitemapMissingPath
  SitemapUnexpectedExtension
  SitemapDuplicate
  SitemapContainsQuery
  SitemapContainsFragment
} derive(Eq, Debug)

///|
pub(all) struct SitemapIssue {
  kind : SitemapIssueKind
  sitemap : String
  message : String
} derive(Eq, Debug)

///|
pub(all) struct SitemapEntry {
  raw : String
  scheme : SitemapScheme
  host : String
  path : String
  normalized_path : String
  format : SitemapFormat
  secure : Bool
  absolute : Bool
  query : String?
  fragment : String?
  depth : Int
  issue_count : Int
  issues : Array[SitemapIssue]
} derive(Eq, Debug)

///|
pub(all) struct SitemapSummary {
  entries : Array[SitemapEntry]
  total : Int
  https_count : Int
  http_count : Int
  relative_count : Int
  xml_count : Int
  gzip_count : Int
  index_count : Int
  issue_count : Int
  duplicate_count : Int
  unique_hosts : Array[String]
  notes : Array[String]
} derive(Eq, Debug)

///|
pub fn SitemapScheme::label(self : SitemapScheme) -> String {
  match self {
    SitemapHttp => "http"
    SitemapHttps => "https"
    SitemapRelative => "relative"
    SitemapUnknown => "unknown"
  }
}

///|
pub fn SitemapFormat::label(self : SitemapFormat) -> String {
  match self {
    SitemapXml => "xml"
    SitemapXmlGzip => "xml-gzip"
    SitemapIndex => "sitemap-index"
    SitemapText => "text"
    SitemapUnknownFormat => "unknown"
  }
}

///|
pub fn SitemapIssueKind::label(self : SitemapIssueKind) -> String {
  match self {
    SitemapMissingHost => "missing-host"
    SitemapInsecureHttp => "insecure-http"
    SitemapUnsupportedScheme => "unsupported-scheme"
    SitemapMissingPath => "missing-path"
    SitemapUnexpectedExtension => "unexpected-extension"
    SitemapDuplicate => "duplicate"
    SitemapContainsQuery => "contains-query"
    SitemapContainsFragment => "contains-fragment"
  }
}

///|
pub fn parse_sitemap_url(raw : StringView) -> SitemapEntry {
  let text = raw.trim().to_owned()
  let ref_parts = split_sitemap_reference(text)
  let path_snapshot = normalize_target(ref_parts.path)
  let format = classify_sitemap_format(path_snapshot.normalized)
  let issues = validate_sitemap_parts(text, ref_parts, path_snapshot, format)
  {
    raw: text,
    scheme: ref_parts.scheme,
    host: ref_parts.host,
    path: ref_parts.path,
    normalized_path: path_snapshot.normalized,
    format,
    secure: ref_parts.scheme == SitemapHttps,
    absolute: ref_parts.scheme == SitemapHttp ||
    ref_parts.scheme == SitemapHttps,
    query: ref_parts.query,
    fragment: ref_parts.fragment,
    depth: path_depth(path_snapshot.normalized),
    issue_count: issues.length(),
    issues,
  }
}

///|
pub fn parse_sitemaps(raw_sitemaps : Array[String]) -> Array[SitemapEntry] {
  let entries : Array[SitemapEntry] = []
  for sitemap in raw_sitemaps {
    entries.push(parse_sitemap_url(sitemap))
  }
  mark_duplicate_sitemaps(entries)
}

///|
pub fn RobotsPolicy::sitemap_entries(
  self : RobotsPolicy,
) -> Array[SitemapEntry] {
  parse_sitemaps(self.sitemaps)
}

///|
pub fn RobotsPolicy::sitemap_summary(self : RobotsPolicy) -> SitemapSummary {
  summarize_sitemaps(self.sitemaps)
}

///|
pub fn summarize_sitemaps(raw_sitemaps : Array[String]) -> SitemapSummary {
  let entries = parse_sitemaps(raw_sitemaps)
  let hosts : Array[String] = []
  let notes : Array[String] = []
  let mut https_count = 0
  let mut http_count = 0
  let mut relative_count = 0
  let mut xml_count = 0
  let mut gzip_count = 0
  let mut index_count = 0
  let mut issue_count = 0
  let mut duplicate_count = 0
  for entry in entries {
    match entry.scheme {
      SitemapHttps => https_count = https_count + 1
      SitemapHttp => http_count = http_count + 1
      SitemapRelative => relative_count = relative_count + 1
      SitemapUnknown => ()
    }
    match entry.format {
      SitemapXml => xml_count = xml_count + 1
      SitemapXmlGzip => gzip_count = gzip_count + 1
      SitemapIndex => index_count = index_count + 1
      _ => ()
    }
    issue_count = issue_count + entry.issue_count
    if entry.has_issue(SitemapDuplicate) {
      duplicate_count = duplicate_count + 1
    }
    if !entry.host.is_empty() {
      add_sitemap_host(hosts, entry.host)
    }
  }
  if entries.is_empty() {
    notes.push("no sitemap directive was found")
  }
  if http_count > 0 {
    notes.push("some sitemap URLs use plain HTTP")
  }
  if relative_count > 0 {
    notes.push("relative sitemap paths rely on the site host")
  }
  if duplicate_count > 0 {
    notes.push("duplicate sitemap URLs were found")
  }
  if issue_count == 0 && !entries.is_empty() {
    notes.push("all sitemap directives passed local structural checks")
  }
  {
    entries,
    total: entries.length(),
    https_count,
    http_count,
    relative_count,
    xml_count,
    gzip_count,
    index_count,
    issue_count,
    duplicate_count,
    unique_hosts: hosts,
    notes,
  }
}

///|
pub fn SitemapEntry::has_issue(
  self : SitemapEntry,
  kind : SitemapIssueKind,
) -> Bool {
  self.issues.any(issue => issue.kind == kind)
}

///|
pub fn SitemapEntry::is_xml_like(self : SitemapEntry) -> Bool {
  self.format == SitemapXml ||
  self.format == SitemapXmlGzip ||
  self.format == SitemapIndex
}

///|
pub fn SitemapEntry::is_cross_host(
  self : SitemapEntry,
  host : StringView,
) -> Bool {
  !self.host.is_empty() && self.host != host.to_owned()
}

///|
pub fn SitemapEntry::to_line(self : SitemapEntry) -> String {
  self.scheme.label() +
  " " +
  self.host +
  self.normalized_path +
  " format=" +
  self.format.label() +
  " issues=" +
  self.issue_count.to_string()
}

///|
pub fn SitemapEntry::to_markdown_row(self : SitemapEntry) -> String {
  "| " +
  self.raw.replace_all(old="|", new="\\|") +
  " | " +
  self.scheme.label() +
  " | " +
  self.host.replace_all(old="|", new="\\|") +
  " | " +
  self.normalized_path.replace_all(old="|", new="\\|") +
  " | " +
  self.format.label() +
  " | " +
  self.issue_count.to_string() +
  " |"
}

///|
pub fn SitemapEntry::issues_text(self : SitemapEntry) -> String {
  if self.issues.is_empty() {
    "none"
  } else {
    let labels : Array[String] = []
    for issue in self.issues {
      labels.push(issue.kind.label())
    }
    labels.join(",")
  }
}

///|
pub fn SitemapIssue::to_line(self : SitemapIssue) -> String {
  self.kind.label() + ": " + self.sitemap + " - " + self.message
}

///|
pub fn SitemapSummary::to_line(self : SitemapSummary) -> String {
  "sitemaps=" +
  self.total.to_string() +
  " https=" +
  self.https_count.to_string() +
  " http=" +
  self.http_count.to_string() +
  " issues=" +
  self.issue_count.to_string()
}

///|
pub fn SitemapSummary::to_markdown(self : SitemapSummary) -> String {
  let lines : Array[String] = []
  lines.push("# RoboPolicy Sitemap Summary")
  lines.push("")
  lines.push("- Total: " + self.total.to_string())
  lines.push("- HTTPS: " + self.https_count.to_string())
  lines.push("- HTTP: " + self.http_count.to_string())
  lines.push("- Relative: " + self.relative_count.to_string())
  lines.push("- XML: " + self.xml_count.to_string())
  lines.push("- XML gzip: " + self.gzip_count.to_string())
  lines.push("- Sitemap index: " + self.index_count.to_string())
  lines.push("- Issues: " + self.issue_count.to_string())
  lines.push("- Duplicates: " + self.duplicate_count.to_string())
  lines.push("- Hosts: " + join_or_none(self.unique_hosts))
  lines.push("")
  lines.push("| URL | Scheme | Host | Path | Format | Issues |")
  lines.push("| --- | --- | --- | --- | --- | --- |")
  for entry in self.entries {
    lines.push(entry.to_markdown_row())
  }
  lines.push("")
  if self.notes.is_empty() {
    lines.push("No sitemap notes.")
  } else {
    lines.push("Notes:")
    for note in self.notes {
      lines.push("- " + note)
    }
  }
  lines.join("\n")
}

///|
pub fn SitemapSummary::issue_lines(self : SitemapSummary) -> Array[String] {
  let lines : Array[String] = []
  for entry in self.entries {
    for issue in entry.issues {
      lines.push(issue.to_line())
    }
  }
  lines
}

///|
pub fn SitemapSummary::has_only_secure_absolute_urls(
  self : SitemapSummary,
) -> Bool {
  self.total > 0 && self.total == self.https_count && self.issue_count == 0
}

///|
pub fn SitemapSummary::has_cross_host_entries(
  self : SitemapSummary,
  expected_host : StringView,
) -> Bool {
  self.entries.any(entry => entry.is_cross_host(expected_host))
}

///|
pub fn sitemap_host_report(entries : Array[SitemapEntry]) -> String {
  let hosts : Array[String] = []
  for entry in entries {
    if !entry.host.is_empty() {
      add_sitemap_host(hosts, entry.host)
    }
  }
  if hosts.is_empty() {
    "no-hosts"
  } else {
    hosts.join(",")
  }
}

///|
pub fn sitemap_issue_report(entries : Array[SitemapEntry]) -> String {
  let lines : Array[String] = []
  for entry in entries {
    if entry.issues.is_empty() {
      lines.push(entry.raw + ": ok")
    } else {
      lines.push(entry.raw + ": " + entry.issues_text())
    }
  }
  lines.join("\n")
}

///|
struct SitemapReferenceParts {
  scheme : SitemapScheme
  host : String
  path : String
  query : String?
  fragment : String?
} derive(Eq, Debug)

///|
fn split_sitemap_reference(raw : String) -> SitemapReferenceParts {
  if raw.has_prefix("https://") {
    split_absolute_sitemap(raw, SitemapHttps, "https://")
  } else if raw.has_prefix("http://") {
    split_absolute_sitemap(raw, SitemapHttp, "http://")
  } else if raw.has_prefix("/") {
    let parts = split_sitemap_query_fragment(raw)
    {
      scheme: SitemapRelative,
      host: "",
      path: parts.path,
      query: parts.query,
      fragment: parts.fragment,
    }
  } else {
    let parts = split_sitemap_query_fragment(raw)
    {
      scheme: SitemapUnknown,
      host: "",
      path: parts.path,
      query: parts.query,
      fragment: parts.fragment,
    }
  }
}

///|
fn split_absolute_sitemap(
  raw : String,
  scheme : SitemapScheme,
  prefix : String,
) -> SitemapReferenceParts {
  let after_scheme = match raw.strip_prefix(prefix) {
    Some(rest) => rest
    None => raw
  }
  match after_scheme.split_once("/") {
    Some((host, path)) => {
      let parts = split_sitemap_query_fragment("/" + path.to_owned())
      {
        scheme,
        host: host.to_owned(),
        path: parts.path,
        query: parts.query,
        fragment: parts.fragment,
      }
    }
    None =>
      {
        scheme,
        host: after_scheme.to_owned(),
        path: "/",
        query: None,
        fragment: None,
      }
  }
}

///|
fn split_sitemap_query_fragment(raw : String) -> QueryFragmentParts {
  let mut path = raw
  let mut query : String? = None
  let mut fragment : String? = None
  let mut had_query = false
  let mut had_fragment = false
  match path.split_once("#") {
    Some((left, right)) => {
      had_fragment = true
      fragment = Some(right.to_owned())
      path = left.to_owned()
    }
    None => ()
  }
  match path.split_once("?") {
    Some((left, right)) => {
      had_query = true
      query = Some(right.to_owned())
      path = left.to_owned()
    }
    None => ()
  }
  { path, query, fragment, had_query, had_fragment }
}

///|
fn classify_sitemap_format(path : String) -> SitemapFormat {
  let lower = path.to_lower()
  if lower.has_suffix(".xml.gz") {
    SitemapXmlGzip
  } else if lower.contains("sitemap_index") || lower.contains("sitemap-index") {
    SitemapIndex
  } else if lower.has_suffix(".xml") {
    SitemapXml
  } else if lower.has_suffix(".txt") {
    SitemapText
  } else {
    SitemapUnknownFormat
  }
}

///|
fn validate_sitemap_parts(
  raw : String,
  parts : SitemapReferenceParts,
  snapshot : PathSnapshot,
  format : SitemapFormat,
) -> Array[SitemapIssue] {
  let issues : Array[SitemapIssue] = []
  if parts.scheme == SitemapUnknown {
    issues.push(
      sitemap_issue(
        SitemapUnsupportedScheme,
        raw,
        "sitemap URL should be absolute HTTP(S) or root-relative",
      ),
    )
  }
  if parts.scheme == SitemapHttp {
    issues.push(
      sitemap_issue(
        SitemapInsecureHttp,
        raw,
        "HTTPS sitemap URLs are preferred",
      ),
    )
  }
  if (parts.scheme == SitemapHttp || parts.scheme == SitemapHttps) &&
    parts.host.is_empty() {
    issues.push(
      sitemap_issue(SitemapMissingHost, raw, "absolute sitemap URL has no host"),
    )
  }
  if snapshot.normalized == "/" {
    issues.push(
      sitemap_issue(
        SitemapMissingPath,
        raw,
        "sitemap URL should point to an explicit sitemap resource",
      ),
    )
  }
  if format == SitemapUnknownFormat {
    issues.push(
      sitemap_issue(
        SitemapUnexpectedExtension,
        raw,
        "sitemap extension is not recognized",
      ),
    )
  }
  if parts.query is Some(_) {
    issues.push(
      sitemap_issue(
        SitemapContainsQuery,
        raw,
        "query strings reduce sitemap cacheability",
      ),
    )
  }
  if parts.fragment is Some(_) {
    issues.push(
      sitemap_issue(
        SitemapContainsFragment,
        raw,
        "fragments are not useful for sitemap URLs",
      ),
    )
  }
  issues
}

///|
fn mark_duplicate_sitemaps(
  entries : Array[SitemapEntry],
) -> Array[SitemapEntry] {
  let result : Array[SitemapEntry] = []
  let seen : Array[String] = []
  for entry in entries {
    let key = sitemap_key(entry)
    if seen.contains(key) {
      result.push(
        add_sitemap_issue(
          entry,
          SitemapDuplicate,
          "same sitemap URL appears more than once",
        ),
      )
    } else {
      seen.push(key)
      result.push(entry)
    }
  }
  result
}

///|
fn add_sitemap_issue(
  entry : SitemapEntry,
  kind : SitemapIssueKind,
  message : String,
) -> SitemapEntry {
  let issues = entry.issues
  issues.push(sitemap_issue(kind, entry.raw, message))
  {
    raw: entry.raw,
    scheme: entry.scheme,
    host: entry.host,
    path: entry.path,
    normalized_path: entry.normalized_path,
    format: entry.format,
    secure: entry.secure,
    absolute: entry.absolute,
    query: entry.query,
    fragment: entry.fragment,
    depth: entry.depth,
    issue_count: issues.length(),
    issues,
  }
}

///|
fn sitemap_key(entry : SitemapEntry) -> String {
  entry.scheme.label() + "://" + entry.host.to_lower() + entry.normalized_path
}

///|
fn sitemap_issue(
  kind : SitemapIssueKind,
  sitemap : String,
  message : String,
) -> SitemapIssue {
  { kind, sitemap, message }
}

///|
fn add_sitemap_host(hosts : Array[String], host : String) -> Unit {
  let lowered = host.to_lower()
  if !hosts.contains(lowered) {
    hosts.push(lowered)
  }
}

///|
fn join_or_none(items : Array[String]) -> String {
  if items.is_empty() {
    "none"
  } else {
    items.join(",")
  }
}