///|
/// One capability needed by a route, component, or user journey.
pub struct CapabilityNeed {
  feature : String
  target_origins : Array[String]
  reason : String
  required : Bool
} derive(Eq, @debug.Debug)

///|
/// A scenario-specific contract for browser capabilities.
pub struct CapabilityContract {
  name : String
  document_origin : String
  needs : Array[CapabilityNeed]
  forbid_unspecified : Bool
} derive(Eq, @debug.Debug)

///|
/// One finding produced while checking a capability contract.
pub struct CapabilityFinding {
  severity : Severity
  code : String
  feature : String
  target_origin : String
  message : String
} derive(Eq, @debug.Debug)

///|
/// Result of auditing a Permissions-Policy against a capability contract.
pub struct CapabilityContractReport {
  contract : CapabilityContract
  ok : Bool
  findings : Array[CapabilityFinding]
  allowed : Int
  missing : Int
  overbroad : Int
  unexpected : Int
} derive(Eq, @debug.Debug)

///|
/// Declare a required browser capability for one or more target origins.
pub fn capability_need(
  feature : String,
  target_origins : Array[String],
  reason : String,
) -> CapabilityNeed {
  { feature, target_origins, reason, required: true }
}

///|
/// Declare an optional browser capability for one or more target origins.
pub fn optional_capability_need(
  feature : String,
  target_origins : Array[String],
  reason : String,
) -> CapabilityNeed {
  { feature, target_origins, reason, required: false }
}

///|
/// Create a named capability contract.
pub fn capability_contract(
  name : String,
  document_origin : String,
  needs : Array[CapabilityNeed],
  forbid_unspecified : Bool,
) -> CapabilityContract {
  normalize_capability_contract({
    name,
    document_origin,
    needs,
    forbid_unspecified,
  })
}

///|
/// Return the declared feature names in stable first-seen order.
pub fn capability_contract_features(
  contract : CapabilityContract,
) -> Array[String] {
  let normalized = normalize_capability_contract(contract)
  let features : Array[String] = []
  for need in normalized.needs {
    push_unique(features, need.feature)
  }
  features
}

///|
/// Build the strictest Permissions-Policy header that satisfies a contract.
pub fn minimal_policy_for_contract(contract : CapabilityContract) -> String {
  build(minimal_policy_intents(contract))
}

///|
/// Build policy intents from a capability contract and the known feature catalog.
pub fn minimal_policy_intents(
  contract : CapabilityContract,
) -> Array[PolicyIntent] {
  let normalized = normalize_capability_contract(contract)
  let intents : Array[PolicyIntent] = []
  let covered : Array[String] = []
  for spec in known_features() {
    push_unique(covered, spec.feature)
    intents.push(contract_intent_for_feature(normalized, spec.feature))
  }
  for feature in capability_contract_features(normalized) {
    if !string_in(covered, feature) {
      intents.push(contract_intent_for_feature(normalized, feature))
    }
  }
  intents
}

///|
/// Audit whether a policy exactly supports a route-level capability contract.
pub fn audit_capability_contract(
  policy : Policy,
  contract : CapabilityContract,
) -> CapabilityContractReport {
  let normalized = normalize_capability_contract(contract)
  let findings : Array[CapabilityFinding] = []
  let checked : Array[String] = []
  let mut allowed = 0
  let mut missing = 0
  let mut overbroad = 0
  let mut unexpected = 0
  for warning in policy.warnings {
    findings.push({
      severity: SeverityWarning,
      code: "policy-parse-warning",
      feature: "-",
      target_origin: "-",
      message: "segment " + warning.index.to_string() + ": " + warning.message,
    })
  }
  for need in normalized.needs {
    if string_in(checked, need.feature) {
      continue
    }
    push_unique(checked, need.feature)
    match combined_need_for_feature(normalized, need.feature) {
      None => ()
      Some(combined) => {
        let counters = audit_declared_capability(
          policy, normalized, combined, findings,
        )
        allowed = allowed + counters.allowed
        missing = missing + counters.missing
        overbroad = overbroad + counters.overbroad
        unexpected = unexpected + counters.unexpected
      }
    }
  }
  if normalized.forbid_unspecified {
    let counters = audit_unspecified_capabilities(policy, normalized, findings)
    unexpected = unexpected + counters.unexpected
  }
  if findings.length() == 0 {
    findings.push({
      severity: SeverityInfo,
      code: "capability-contract-satisfied",
      feature: "-",
      target_origin: "-",
      message: "policy satisfies declared browser capability needs",
    })
  }
  {
    contract: normalized,
    ok: !capability_has_high(findings),
    findings,
    allowed,
    missing,
    overbroad,
    unexpected,
  }
}

///|
/// Render a contract audit in stable line-oriented text for CI logs.
pub fn render_capability_contract_report(
  report : CapabilityContractReport,
) -> String {
  let lines : Array[String] = []
  lines.push("permscope capability contract")
  lines.push("contract=" + report.contract.name)
  lines.push("document_origin=" + report.contract.document_origin)
  lines.push(
    "allowed=" +
    report.allowed.to_string() +
    " missing=" +
    report.missing.to_string() +
    " overbroad=" +
    report.overbroad.to_string() +
    " unexpected=" +
    report.unexpected.to_string(),
  )
  if report.ok {
    lines.push("contract: pass")
  } else {
    lines.push("contract: fail")
  }
  for finding in report.findings {
    lines.push(
      severity_label(finding.severity) +
      " " +
      finding.code +
      " " +
      finding.feature +
      " " +
      finding.target_origin +
      " - " +
      finding.message,
    )
  }
  lines.join("\n")
}

///|
struct ContractCounters {
  allowed : Int
  missing : Int
  overbroad : Int
  unexpected : Int
} derive(Eq, @debug.Debug)

///|
fn empty_contract_counters() -> ContractCounters {
  { allowed: 0, missing: 0, overbroad: 0, unexpected: 0 }
}

///|
fn audit_declared_capability(
  policy : Policy,
  contract : CapabilityContract,
  need : CapabilityNeed,
  findings : Array[CapabilityFinding],
) -> ContractCounters {
  let mut counters = empty_contract_counters()
  for target in need_targets(need, contract.document_origin) {
    if allows(policy, need.feature, contract.document_origin, target) {
      counters = { ..counters, allowed: counters.allowed + 1 }
    } else if need.required {
      counters = { ..counters, missing: counters.missing + 1 }
      findings.push({
        severity: SeverityHigh,
        code: "required-capability-blocked",
        feature: need.feature,
        target_origin: target,
        message: "declared required capability is blocked by the policy: " +
        need.reason,
      })
    } else {
      findings.push({
        severity: SeverityInfo,
        code: "optional-capability-blocked",
        feature: need.feature,
        target_origin: target,
        message: "optional capability is blocked by the policy: " + need.reason,
      })
    }
  }
  match directive(policy, need.feature) {
    None => ()
    Some(item) => {
      if has_wildcard(item.tokens) {
        counters = { ..counters, overbroad: counters.overbroad + 1 }
        findings.push({
          severity: SeverityHigh,
          code: "capability-wildcard",
          feature: need.feature,
          target_origin: "*",
          message: "declared capability is delegated to every origin",
        })
      }
      if contract.forbid_unspecified {
        let extra = audit_extra_tokens(contract, need, item, findings)
        counters = {
          ..counters,
          unexpected: counters.unexpected + extra.unexpected,
        }
      }
    }
  }
  counters
}

///|
fn audit_extra_tokens(
  contract : CapabilityContract,
  need : CapabilityNeed,
  item : Directive,
  findings : Array[CapabilityFinding],
) -> ContractCounters {
  let mut counters = empty_contract_counters()
  for token in item.tokens {
    match token {
      TokenAll => ()
      TokenSelf =>
        if !need_allows_target(need, contract.document_origin) {
          counters = { ..counters, unexpected: counters.unexpected + 1 }
          findings.push({
            severity: SeverityWarning,
            code: "unexpected-self-delegation",
            feature: need.feature,
            target_origin: contract.document_origin,
            message: "policy allows self but the contract did not declare it",
          })
        }
      TokenOrigin(origin) => {
        let target = normalize_origin(origin)
        if !need_allows_target(need, target) {
          counters = { ..counters, unexpected: counters.unexpected + 1 }
          findings.push({
            severity: feature_risk_severity(need.feature),
            code: "unexpected-origin-delegation",
            feature: need.feature,
            target_origin: target,
            message: "policy delegates this capability to an undeclared origin",
          })
        }
      }
    }
  }
  counters
}

///|
fn audit_unspecified_capabilities(
  policy : Policy,
  contract : CapabilityContract,
  findings : Array[CapabilityFinding],
) -> ContractCounters {
  let mut counters = empty_contract_counters()
  for item in policy.directives {
    if !contract_declares_feature(contract, item.feature) &&
      item.tokens.length() > 0 {
      counters = { ..counters, unexpected: counters.unexpected + 1 }
      findings.push({
        severity: feature_risk_severity(item.feature),
        code: "undeclared-feature-enabled",
        feature: item.feature,
        target_origin: token_scope_label(item.tokens),
        message: "policy enables a feature not declared by the capability contract",
      })
    }
  }
  for spec in known_features() {
    if !contract_declares_feature(contract, spec.feature) &&
      allows(
        policy,
        spec.feature,
        contract.document_origin,
        "https://cross-site.invalid",
      ) {
      counters = { ..counters, unexpected: counters.unexpected + 1 }
      findings.push({
        severity: feature_risk_severity(spec.feature),
        code: "undeclared-feature-default-open",
        feature: spec.feature,
        target_origin: "https://cross-site.invalid",
        message: "missing directive leaves an undeclared feature open by default",
      })
    }
  }
  counters
}

///|
fn contract_intent_for_feature(
  contract : CapabilityContract,
  feature : String,
) -> PolicyIntent {
  match combined_need_for_feature(contract, feature) {
    None => deny(feature)
    Some(need) => {
      let targets = need_targets(need, contract.document_origin)
      let external : Array[String] = []
      let mut include_self = false
      for target in targets {
        if target == contract.document_origin {
          include_self = true
        } else {
          push_unique(external, target)
        }
      }
      if include_self && external.length() == 0 {
        self_only(feature)
      } else if include_self {
        self_and_origins(feature, external)
      } else if external.length() > 0 {
        origins_only(feature, external)
      } else {
        deny(feature)
      }
    }
  }
}

///|
fn combined_need_for_feature(
  contract : CapabilityContract,
  feature : String,
) -> CapabilityNeed? {
  let wanted = normalize_feature(feature)
  let targets : Array[String] = []
  let reasons : Array[String] = []
  let mut required = false
  let mut found = false
  for need in contract.needs {
    if need.feature == wanted {
      found = true
      if need.required {
        required = true
      }
      for target in need_targets(need, contract.document_origin) {
        push_unique(targets, target)
      }
      if need.reason != "" {
        reasons.push(need.reason)
      }
    }
  }
  if found {
    Some({
      feature: wanted,
      target_origins: targets,
      reason: reasons.join("; "),
      required,
    })
  } else {
    None
  }
}

///|
fn normalize_capability_contract(
  contract : CapabilityContract,
) -> CapabilityContract {
  let document_origin = normalize_origin(contract.document_origin)
  let needs : Array[CapabilityNeed] = []
  for need in contract.needs {
    needs.push(normalize_capability_need(need, document_origin))
  }
  {
    name: trim(contract.name),
    document_origin,
    needs,
    forbid_unspecified: contract.forbid_unspecified,
  }
}

///|
fn normalize_capability_need(
  need : CapabilityNeed,
  document_origin : String,
) -> CapabilityNeed {
  let targets : Array[String] = []
  for target in need.target_origins {
    let normalized = normalize_contract_target(target, document_origin)
    if normalized != "" {
      push_unique(targets, normalized)
    }
  }
  {
    feature: normalize_feature(need.feature),
    target_origins: targets,
    reason: trim(need.reason),
    required: need.required,
  }
}

///|
fn normalize_contract_target(
  target : String,
  document_origin : String,
) -> String {
  let clean = strip_quotes(trim(target)).to_lower()
  if clean == "" || clean == "self" {
    document_origin
  } else {
    normalize_origin(clean)
  }
}

///|
fn need_targets(
  need : CapabilityNeed,
  document_origin : String,
) -> Array[String] {
  if need.target_origins.length() == 0 {
    [document_origin]
  } else {
    need.target_origins
  }
}

///|
fn need_allows_target(need : CapabilityNeed, target_origin : String) -> Bool {
  let target = normalize_origin(target_origin)
  for declared in need.target_origins {
    if normalize_origin(declared) == target {
      return true
    }
  }
  false
}

///|
fn contract_declares_feature(
  contract : CapabilityContract,
  feature : String,
) -> Bool {
  let wanted = normalize_feature(feature)
  for need in contract.needs {
    if need.feature == wanted {
      return true
    }
  }
  false
}

///|
fn feature_risk_severity(feature : String) -> Severity {
  match known_feature(feature) {
    Some(spec) =>
      if spec.risk == RiskHigh {
        SeverityHigh
      } else {
        SeverityWarning
      }
    None => SeverityWarning
  }
}

///|
fn token_scope_label(tokens : Array[AllowToken]) -> String {
  if has_wildcard(tokens) {
    "*"
  } else if tokens.length() == 1 && tokens[0] == TokenSelf {
    "self"
  } else {
    "explicit"
  }
}

///|
fn capability_has_high(findings : Array[CapabilityFinding]) -> Bool {
  for finding in findings {
    if finding.severity == SeverityHigh {
      return true
    }
  }
  false
}