// el-auth — Built-in authentication and authorization for el-ui.
//
// Engram-aware sessions: server-side invalidation works even with stateless
// JWTs because every session is also a graph node.
//
// Provider trait surface:
//   verify(token)        -> AuthContext
//   issue(user, roles)   -> token string
//   revoke(token)        -> Bool

// ── Errors ────────────────────────────────────────────────────────────────────

let ERR_INVALID_CREDS:    String = "auth.invalid_credentials"
let ERR_TOKEN_EXPIRED:    String = "auth.token_expired"
let ERR_TOKEN_INVALID:    String = "auth.token_invalid"
let ERR_SESSION_NOT_FOUND: String = "auth.session_not_found"
let ERR_FORBIDDEN:        String = "auth.forbidden"
let ERR_CONFIG:           String = "auth.config"

// ── AuthContext + AuthUser ────────────────────────────────────────────────────

type AuthUser {
    id:           String
    email:        String
    display_name: String
}

type AuthContext {
    user_id:     String
    email:       String
    roles:       String   // JSON array of role names
    permissions: String   // JSON array of permission strings
    session_id:  String
    issued_at:   String
    expires_at:  String
}

fn auth_context_empty() -> AuthContext {
    { "user_id": "", "email": "", "roles": "[]", "permissions": "[]",
      "session_id": "", "issued_at": "", "expires_at": "" }
}

fn auth_context_has_permission(ctx: AuthContext, perm: String) -> Bool {
    str_contains(ctx.permissions, "\"" + perm + "\"")
}

fn auth_context_has_role(ctx: AuthContext, role: String) -> Bool {
    str_contains(ctx.roles, "\"" + role + "\"")
}

// ── Roles + permissions ───────────────────────────────────────────────────────

type Permission {
    resource: String
    action:   String
}

fn permission_new(resource: String, action: String) -> Permission {
    { "resource": resource, "action": action }
}

fn permission_string(p: Permission) -> String {
    p.resource + ":" + p.action
}

// Role registry — maps role name -> JSON array of permission strings.
fn role_registry_grant(registry_path: String, role: String, perm: String) -> Bool {
    let raw: String = fs_read(registry_path)
    let updated: String = json_array_append(raw, role, "\"" + perm + "\"")
    fs_write(registry_path, updated)
}

// ── JWT (HS256) ──────────────────────────────────────────────────────────────

type JwtClaims {
    sub: String   // user id
    iss: String   // issuer
    aud: String   // audience
    iat: Int      // issued at  (unix seconds)
    exp: Int      // expires at (unix seconds)
    jti: String   // unique token id
}

fn jwt_claims_new(user_id: String, issuer: String, audience: String, ttl_seconds: Int) -> JwtClaims {
    let now: Int = time_now_unix()
    { "sub": user_id, "iss": issuer, "aud": audience,
      "iat": now, "exp": now + ttl_seconds, "jti": uuid_v4() }
}

fn jwt_encode(claims: JwtClaims, secret: String) -> String {
    let header_b64: String = base64url_no_pad("{\"alg\":\"HS256\",\"typ\":\"JWT\"}")
    let payload_json: String = json_encode(claims)
    let payload_b64: String = base64url_no_pad(payload_json)
    let signing_input: String = header_b64 + "." + payload_b64
    let sig: String = base64url_no_pad(hmac_sha256(secret, signing_input))
    signing_input + "." + sig
}

fn jwt_decode(token: String, secret: String) -> AuthContext {
    let parts:   String = token   // [header].[payload].[sig]
    let dot1:    Int = str_index_of(parts, ".")
    if dot1 < 0 { return auth_context_empty() }
    let rest:    String = str_slice(parts, dot1 + 1, str_len(parts))
    let dot2:    Int = str_index_of(rest, ".")
    if dot2 < 0 { return auth_context_empty() }
    let header_b64:  String = str_slice(parts, 0, dot1)
    let payload_b64: String = str_slice(rest, 0, dot2)
    let sig_b64:     String = str_slice(rest, dot2 + 1, str_len(rest))

    let signing_input: String = header_b64 + "." + payload_b64
    let expected_sig:  String = base64url_no_pad(hmac_sha256(secret, signing_input))
    if !str_eq(expected_sig, sig_b64) { return auth_context_empty() }

    let payload_json: String = base64url_decode(payload_b64)
    let now: Int = time_now_unix()
    let exp: Int = str_to_int(json_get(payload_json, "exp"))
    if exp < now { return auth_context_empty() }

    { "user_id":   json_get(payload_json, "sub"),
      "email":     "",
      "roles":     "[]",
      "permissions": "[]",
      "session_id": json_get(payload_json, "jti"),
      "issued_at": json_get(payload_json, "iat"),
      "expires_at": json_get(payload_json, "exp") }
}

// ── Engram-backed session store ──────────────────────────────────────────────
//
// Sessions are nodes of type "Session" connected to User via has_session.
// Revocation = node delete. Verification = node lookup + expiry check.

fn session_store_create(user_id: String, ttl_seconds: Int, ip: String) -> String {
    let now:  String = time_now_iso()
    let exp:  String = time_add_seconds(now, ttl_seconds)
    let id:   String = uuid_v4()
    let body: String = "{\"id\":\"" + id + "\",\"user_id\":\"" + user_id
        + "\",\"created_at\":\"" + now + "\",\"expires_at\":\"" + exp
        + "\",\"ip_address\":\"" + ip + "\"}"
    let node_id: String = engram_node_create("Session", body)
    engram_edge_create(user_id, node_id, "has_session")
    node_id
}

fn session_store_verify(session_id: String) -> Bool {
    let raw: String = engram_node_get(session_id)
    if str_eq(raw, "") { return false }
    let exp: String = json_get(raw, "expires_at")
    !time_after(time_now_iso(), exp)
}

fn session_store_revoke(session_id: String) -> Bool {
    engram_node_delete(session_id)
}

// ── Middleware ────────────────────────────────────────────────────────────────
//
// auth_middleware extracts the bearer token, decodes it, and populates the
// AuthContext. Applied automatically by the @authenticate aspect (el-aop).

fn extract_bearer(authorization_header: String) -> String {
    if str_starts_with(authorization_header, "Bearer ") {
        return str_slice(authorization_header, 7, str_len(authorization_header))
    }
    ""
}

fn auth_middleware(authorization_header: String, jwt_secret: String) -> AuthContext {
    let token: String = extract_bearer(authorization_header)
    if str_eq(token, "") { return auth_context_empty() }
    jwt_decode(token, jwt_secret)
}

fn enforce_permission(ctx: AuthContext, required_perm: String) -> Bool {
    if str_eq(ctx.user_id, "") { return false }
    auth_context_has_permission(ctx, required_perm)
}

// ── Provider issue/verify ────────────────────────────────────────────────────

fn provider_issue(user: AuthUser, jwt_secret: String, issuer: String, audience: String, ttl: Int) -> String {
    let claims: JwtClaims = jwt_claims_new(user.id, issuer, audience, ttl)
    jwt_encode(claims, jwt_secret)
}

fn provider_verify(token: String, jwt_secret: String) -> AuthContext {
    let ctx: AuthContext = jwt_decode(token, jwt_secret)
    if str_eq(ctx.user_id, "") { return ctx }
    if !session_store_verify(ctx.session_id) { return auth_context_empty() }
    ctx
}

fn provider_revoke(session_id: String) -> Bool {
    session_store_revoke(session_id)
}

// ── Entry — smoke test ───────────────────────────────────────────────────────

let user: AuthUser = { "id": "u-001", "email": "will@neurontechnologies.ai", "display_name": "Will" }
let token: String = provider_issue(user, "test-secret", "el-ui", "el-app", 3600)
println("[el-auth] issued JWT for " + user.email)
