// RequestJSON — JSON-friendly Request DTO for Cedar authorization requests.
//
// References:
//   Rust: cedar-policy-core/src/entities.rs
//   Go:   cedar-go/internal/entities/

// ---------------------------------------------------------------------------
// Internal PARC string helpers
// ---------------------------------------------------------------------------

///|
fn entity_type_str(ty : @ast.EntityType) -> String {
  match ty {
    @ast.EntityType(s) => s
  }
}

///|
/// Serialize an EntityUIDEntry to a Cedar text string.
fn parc_to_str(entry : @evaluator.EntityUIDEntry) -> String? {
  match entry {
    @evaluator.Concrete(uid) => Some(uid.type_ + "::\"" + uid.id + "\"")
    @evaluator.Unknown(ty) => {
      let s = entity_type_str(ty)
      if s == "" {
        None
      } else {
        Some(s)
      }
    }
  }
}

///|
/// Parse a Cedar text string → EntityUIDEntry (best-effort, never raises).
fn parc_from_str(s : String?) -> @evaluator.EntityUIDEntry {
  match s {
    None => @evaluator.Unknown(@ast.EntityType(""))
    Some(s) => {
      let mut has_colons = false
      for i = 0; i < s.length() - 1; i = i + 1 {
        if s[i] == ':' && s[i + 1] == ':' {
          has_colons = true
          break
        }
      }
      if has_colons {
        match parc_parse_euid(s) {
          Some(uid) => @evaluator.Concrete(uid)
          None => @evaluator.Unknown(@ast.EntityType(s))
        }
      } else {
        @evaluator.Unknown(@ast.EntityType(s))
      }
    }
  }
}

///|
fn parc_parse_euid(s : String) -> @ast.EntityUID? {
  let len = s.length()
  let mut sep = -1
  let mut idx = len - 3
  while idx >= 0 {
    if s[idx] == ':' && s[idx + 1] == ':' && s[idx + 2] == '"' {
      sep = idx
      break
    }
    idx = idx - 1
  }
  if sep < 0 {
    return None
  }
  let type_ = s[:sep].to_owned()
  let id_start = sep + 3
  let mut id_len = 0
  for i = id_start; i < len; i = i + 1 {
    if s[i] == '"' {
      id_len = i - id_start
      break
    }
  }
  if id_len == 0 {
    return None
  }
  let id = s[id_start:id_start + id_len].to_owned()
  Some(@ast.EntityUID::{ type_, id })
}

// ---------------------------------------------------------------------------
// RequestJSON struct
// ---------------------------------------------------------------------------

///|
/// JSON-friendly representation of an authorization Request.
///
/// ```json
/// {
///   "principal":"GitApp::User::\"JaneDoe\"",
///   "action":"Action::\"view\"",
///   "resource":"Photo::\"x\"",
///   "context":{"is_admin":true}
/// }
/// ```
///
/// - PARC strings: "Type::\"id\"" → Concrete, "Type" → Unknown, null/absent → Unknown("")
/// - PARC objects: {"type":"..","id":".."} → normalised to string
/// - Context absent/empty-object/null → Unknown, otherwise → Concrete
pub(all) struct RequestJSON {
  principal : String?
  action : String?
  resource : String?
  context : @ast.Value?
} derive(Debug, Eq)

///|
/// Convert a Request to its JSON-friendly representation.
pub fn to_request_json(req : @evaluator.Request) -> RequestJSON {
  RequestJSON::{
    principal: parc_to_str(req.principal),
    action: parc_to_str(req.action),
    resource: parc_to_str(req.resource),
    context: match req.context {
      @evaluator.Concrete(v) => Some(v)
      @evaluator.Unknown => None
      @evaluator.Partial(_) => None
    },
  }
}

///|
/// Convert a RequestJSON into a full Request.
pub fn RequestJSON::to_request(self : RequestJSON) -> @evaluator.Request {
  @evaluator.Request::{
    principal: parc_from_str(self.principal),
    action: parc_from_str(self.action),
    resource: parc_from_str(self.resource),
    context: match self.context {
      Some(v) => @evaluator.Concrete(v)
      None => @evaluator.Unknown
    },
  }
}

///|
pub impl ToJson for RequestJSON with fn to_json(self) -> Json {
  let fields : Map[String, Json] = Map([])
  match self.principal {
    Some(s) => fields.set("principal", Json::string(s))
    None => fields.set("principal", Json::string(""))
  }
  match self.action {
    Some(s) => fields.set("action", Json::string(s))
    None => fields.set("action", Json::string(""))
  }
  match self.resource {
    Some(s) => fields.set("resource", Json::string(s))
    None => fields.set("resource", Json::string(""))
  }
  match self.context {
    Some(v) => fields.set("context", v.to_json())
    None => fields.set("context", Json::object(Map([])))
  }
  Json::object(fields)
}

///|
/// Deserialize RequestJSON from JSON. Supports both PARC string and object formats.
pub impl FromJson for RequestJSON with fn from_json(
  json : Json,
  path : @json.JsonPath,
) -> RequestJSON raise @json.JsonDecodeError {
  match json {
    Json::Object(obj) => {
      let principal = match obj.get("principal") {
        Some(j) =>
          match j {
            Json::String(s) => Some(s)
            Json::Object(_) => {
              let uid = @ast.EntityUID::from_json(j, path.add_key("principal"))
              Some(uid.type_ + "::\"" + uid.id + "\"")
            }
            Json::Null => None
            _ =>
              raise @json.JsonDecodeError(
                (path.add_key("principal"), "expected string, object, or null"),
              )
          }
        None => None
      }
      let action = match obj.get("action") {
        Some(j) =>
          match j {
            Json::String(s) => Some(s)
            Json::Object(_) => {
              let uid = @ast.EntityUID::from_json(j, path.add_key("action"))
              Some(uid.type_ + "::\"" + uid.id + "\"")
            }
            Json::Null => None
            _ =>
              raise @json.JsonDecodeError(
                (path.add_key("action"), "expected string, object, or null"),
              )
          }
        None => None
      }
      let resource = match obj.get("resource") {
        Some(j) =>
          match j {
            Json::String(s) => Some(s)
            Json::Object(_) => {
              let uid = @ast.EntityUID::from_json(j, path.add_key("resource"))
              Some(uid.type_ + "::\"" + uid.id + "\"")
            }
            Json::Null => None
            _ =>
              raise @json.JsonDecodeError(
                (path.add_key("resource"), "expected string, object, or null"),
              )
          }
        None => None
      }
      let context = match obj.get("context") {
        Some(j) =>
          match j {
            Json::Null => None
            Json::Object(inner) =>
              if inner.length() == 0 {
                None
              } else {
                Some(cedar_value_from_json(j, path.add_key("context")))
              }
            _ => Some(cedar_value_from_json(j, path.add_key("context")))
          }
        None => None
      }
      RequestJSON::{ principal, action, resource, context }
    }
    _ => raise @json.JsonDecodeError((path, "expected object"))
  }
}