// el-identity — Engram-native identity for el-ui.
//
// Identity in el-ui is a graph, not a table. Every entity is a node; every
// relationship is an edge. Authentication is spreading activation from a
// session token through the identity subgraph until it touches a User node.
//
// Edges:
//   User ──has_role──▶ Role ──grants──▶ Scope
//   User ──has_session──▶ Session ──authenticated_via──▶ OAuthToken

// ── Edge type constants ──────────────────────────────────────────────────────

let EDGE_HAS_ROLE: String           = "has_role"
let EDGE_HAS_SESSION: String        = "has_session"
let EDGE_AUTHENTICATED_VIA: String  = "authenticated_via"
let EDGE_GRANTS: String             = "grants"

// ── Node type constants ──────────────────────────────────────────────────────

let NODE_USER: String        = "User"
let NODE_ROLE: String        = "Role"
let NODE_SCOPE: String       = "Scope"
let NODE_OAUTH_TOKEN: String = "OAuthToken"
let NODE_SESSION: String     = "Session"

// ── User ─────────────────────────────────────────────────────────────────────

type User {
    id:           String
    email:        String
    display_name: String
    created_at:   String
}

fn user_new(email: String, display_name: String) -> User {
    let now: String = time_now_iso()
    let id: String  = uuid_v4()
    { "id": id, "email": email, "display_name": display_name, "created_at": now }
}

// ── Role / Scope ─────────────────────────────────────────────────────────────

type Role {
    id:          String
    name:        String
    permissions: String   // JSON-encoded array; struct fields are flat in El today
}

type Scope {
    id:          String
    name:        String
    description: String
}

fn role_new(name: String) -> Role {
    { "id": uuid_v4(), "name": name, "permissions": "[]" }
}

fn role_has_permission(role: Role, perm: String) -> Bool {
    str_contains(role.permissions, "\"" + perm + "\"")
}

fn scope_new(name: String, description: String) -> Scope {
    { "id": uuid_v4(), "name": name, "description": description }
}

// ── Session ──────────────────────────────────────────────────────────────────

type Session {
    id:         String
    user_id:    String
    created_at: String
    expires_at: String
    ip_address: String
}

fn session_new(user_id: String, ttl_seconds: Int, ip_address: String) -> Session {
    let now: String = time_now_iso()
    let exp: String = time_add_seconds(now, ttl_seconds)
    { "id": uuid_v4(), "user_id": user_id, "created_at": now, "expires_at": exp, "ip_address": ip_address }
}

fn session_is_expired(session: Session) -> Bool {
    time_after(time_now_iso(), session.expires_at)
}

// ── OAuthToken ───────────────────────────────────────────────────────────────
//
// Tokens are SHA-256 hashed before storage — the raw token never persists.

type OAuthToken {
    id:                 String
    provider:           String
    access_token_hash:  String
    refresh_token_hash: String
    expires_at:         String
    scopes:             String   // JSON-encoded array
}

fn token_hash(raw: String) -> String {
    sha256_hex(raw)
}

fn oauth_token_new(provider: String, access_raw: String, refresh_raw: String, expires_at: String, scopes: String) -> OAuthToken {
    let access_h:  String = token_hash(access_raw)
    let refresh_h: String = ""
    if !str_eq(refresh_raw, "") { let refresh_h = token_hash(refresh_raw) }
    { "id": uuid_v4(), "provider": provider, "access_token_hash": access_h,
      "refresh_token_hash": refresh_h, "expires_at": expires_at, "scopes": scopes }
}

fn oauth_token_is_expired(t: OAuthToken) -> Bool {
    time_after(time_now_iso(), t.expires_at)
}

// ── PKCE (RFC 7636) ──────────────────────────────────────────────────────────

type PkceChallenge {
    verifier:  String
    challenge: String
    method:    String   // always "S256"
}

fn pkce_generate() -> PkceChallenge {
    let verifier: String = base64url_no_pad(random_bytes(32))
    let chal:     String = base64url_no_pad(sha256_bytes(verifier))
    { "verifier": verifier, "challenge": chal, "method": "S256" }
}

fn pkce_verify(challenge: String, verifier: String) -> Bool {
    let computed: String = base64url_no_pad(sha256_bytes(verifier))
    str_eq(computed, challenge)
}

// ── OAuth providers (Google / GitHub / Apple) ────────────────────────────────
//
// Stubbed: shape-only. `provider_authorization_url` produces the redirect URL;
// `provider_exchange_code` POSTs to the token endpoint via http_post.

type OAuthProviderCfg {
    name:          String
    client_id:     String
    client_secret: String
    auth_url:      String
    token_url:     String
    default_scopes: String
}

fn google_provider(client_id: String, client_secret: String) -> OAuthProviderCfg {
    { "name": "google", "client_id": client_id, "client_secret": client_secret,
      "auth_url":  "https://accounts.google.com/o/oauth2/v2/auth",
      "token_url": "https://oauth2.googleapis.com/token",
      "default_scopes": "[\"openid\",\"email\",\"profile\"]" }
}

fn github_provider(client_id: String, client_secret: String) -> OAuthProviderCfg {
    { "name": "github", "client_id": client_id, "client_secret": client_secret,
      "auth_url":  "https://github.com/login/oauth/authorize",
      "token_url": "https://github.com/login/oauth/access_token",
      "default_scopes": "[\"read:user\",\"user:email\"]" }
}

fn apple_provider(client_id: String, client_secret: String) -> OAuthProviderCfg {
    { "name": "apple", "client_id": client_id, "client_secret": client_secret,
      "auth_url":  "https://appleid.apple.com/auth/authorize",
      "token_url": "https://appleid.apple.com/auth/token",
      "default_scopes": "[\"name\",\"email\"]" }
}

fn provider_authorization_url(p: OAuthProviderCfg, redirect_uri: String, code_challenge: String, state: String) -> String {
    p.auth_url + "?response_type=code"
        + "&client_id="     + url_encode(p.client_id)
        + "&redirect_uri="  + url_encode(redirect_uri)
        + "&scope="         + url_encode(json_array_to_space_list(p.default_scopes))
        + "&state="         + url_encode(state)
        + "&code_challenge=" + url_encode(code_challenge)
        + "&code_challenge_method=S256"
}

// ── Engram client interface (graph CRUD) ─────────────────────────────────────
//
// Identity persists to the local Engram graph. These wrap engram_* runtime
// calls (planned). Until that lands, the server-side stub uses a JSON file.

fn engram_create_node(node_type: String, value_json: String) -> String {
    engram_node_create(node_type, value_json)
}

fn engram_create_edge(from_id: String, to_id: String, edge_type: String) -> Bool {
    engram_edge_create(from_id, to_id, edge_type)
}

fn engram_find_connected(node_id: String, edge_type: String) -> String {
    engram_edge_traverse(node_id, edge_type)
}

// ── OAuth flow coordinator ───────────────────────────────────────────────────

type AuthFlowParams {
    pkce_verifier: String
    pkce_challenge: String
    redirect_url:  String
    state:         String
}

fn begin_auth_flow(provider: OAuthProviderCfg, redirect_uri: String) -> AuthFlowParams {
    let pkce: PkceChallenge = pkce_generate()
    let state: String       = base64url_no_pad(random_bytes(16))
    let url:   String       = provider_authorization_url(provider, redirect_uri, pkce.challenge, state)
    { "pkce_verifier": pkce.verifier, "pkce_challenge": pkce.challenge, "redirect_url": url, "state": state }
}

fn exchange_code(provider: OAuthProviderCfg, code: String, pkce_verifier: String, redirect_uri: String, session_id: String) -> OAuthToken {
    let body: String = "grant_type=authorization_code"
        + "&code="          + url_encode(code)
        + "&redirect_uri="  + url_encode(redirect_uri)
        + "&client_id="     + url_encode(provider.client_id)
        + "&client_secret=" + url_encode(provider.client_secret)
        + "&code_verifier=" + url_encode(pkce_verifier)
    let resp: String = http_post(provider.token_url, body)
    let access:  String = json_get(resp, "access_token")
    let refresh: String = json_get(resp, "refresh_token")
    let expires_in: Int = str_to_int(json_get(resp, "expires_in"))
    let exp_at:  String = time_add_seconds(time_now_iso(), expires_in)
    let token:   OAuthToken = oauth_token_new(provider.name, access, refresh, exp_at, "[]")
    let token_id: String = engram_create_node(NODE_OAUTH_TOKEN, json_encode(token))
    engram_create_edge(session_id, token_id, EDGE_AUTHENTICATED_VIA)
    token
}

// ── Session manager ──────────────────────────────────────────────────────────

fn session_create(user_id: String, ttl: Int, ip: String) -> Session {
    let s: Session       = session_new(user_id, ttl, ip)
    let session_id: String = engram_create_node(NODE_SESSION, json_encode(s))
    engram_create_edge(user_id, session_id, EDGE_HAS_SESSION)
    s
}

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

// ── AuthGuard — applied via @authenticate decorator ─────────────────────────

fn auth_guard_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)
}

// ── Identity context (passed through the request lifecycle) ─────────────────

type IdentityContext {
    user_id:    String
    session_id: String
    roles_json: String
}

fn identity_load(session_id: String) -> IdentityContext {
    let session_raw: String = engram_node_get(session_id)
    let user_id:     String = json_get(session_raw, "user_id")
    let roles_raw:   String = engram_find_connected(user_id, EDGE_HAS_ROLE)
    { "user_id": user_id, "session_id": session_id, "roles_json": roles_raw }
}

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

let user: User = user_new("will@neurontechnologies.ai", "Will Anderson")
println("[el-identity] user " + user.email + " (" + user.id + ")")
