// el-aop — Aspect-Oriented Programming for el-ui.
//
// Each aspect has three advice points: before, after, around. The aspect
// chain is composed at compile time by the el-ui-compiler from decorators.
//
// The El runtime model uses tagged JSON for InvocationContext so an aspect
// chain composes purely by passing the context map through each advice fn.
//
// Built-in aspects:
//   @authenticate   — defaults on every component (security-by-default)
//   @public         — opt-out marker
//   @authorize      — role/permission gate
//   @cache          — TTL-keyed memoization
//   @rate_limit     — per-principal token bucket
//   @retry          — retry on error with backoff
//   @log            — structured log around invocation
//   @trace          — emit OpenTelemetry-shaped spans
//   @validate       — JSON schema check on args

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

let AOP_ERR_UNAUTHENTICATED:  String = "aop.unauthenticated"
let AOP_ERR_FORBIDDEN:        String = "aop.forbidden"
let AOP_ERR_RATE_LIMITED:     String = "aop.rate_limited"
let AOP_ERR_VALIDATION:       String = "aop.validation_failed"
let AOP_ERR_RETRIES_EXHAUSTED: String = "aop.retries_exhausted"

// ── Invocation context ───────────────────────────────────────────────────────
//
// Stored as JSON: { target, method, args:{}, metadata:{} }
// All advice mutates by returning a new ctx (functional style).

fn ctx_new(target: String, method: String) -> String {
    "{\"target\":\"" + target + "\",\"method\":\"" + method
        + "\",\"args\":{},\"metadata\":{}}"
}

fn ctx_with_arg(ctx: String, key: String, value: String) -> String {
    json_set_path(ctx, "args." + key, "\"" + value + "\"")
}

fn ctx_with_meta(ctx: String, key: String, value: String) -> String {
    json_set_path(ctx, "metadata." + key, "\"" + value + "\"")
}

fn ctx_get_meta(ctx: String, key: String) -> String {
    json_get_path(ctx, "metadata." + key)
}

// ── Aspect dispatch table ────────────────────────────────────────────────────
//
// Each aspect is a triple of fn names: (before, around, after). The registry
// is a map from aspect name -> { before, around, after } JSON.

fn registry_new() -> String {
    "{}"
}

fn registry_register(reg: String, name: String, before_fn: String, around_fn: String, after_fn: String) -> String {
    let entry: String = "{\"before\":\"" + before_fn + "\",\"around\":\"" + around_fn
        + "\",\"after\":\"" + after_fn + "\"}"
    json_set(reg, name, entry)
}

// ── @authenticate — applied by default ───────────────────────────────────────

fn aspect_authenticate_before(ctx: String) -> String {
    let token: String = ctx_get_meta(ctx, "authorization")
    if str_eq(token, "") {
        return ctx_with_meta(ctx, "error", AOP_ERR_UNAUTHENTICATED)
    }
    let secret: String = env("JWT_SECRET")
    let auth_ctx: String = auth_middleware(token, secret)
    let user_id:  String = json_get(auth_ctx, "user_id")
    if str_eq(user_id, "") {
        return ctx_with_meta(ctx, "error", AOP_ERR_UNAUTHENTICATED)
    }
    ctx_with_meta(ctx, "user_id", user_id)
}

// ── @public — explicit opt-out marker ────────────────────────────────────────

fn aspect_public_before(ctx: String) -> String {
    ctx_with_meta(ctx, "public", "true")
}

// ── @authorize(role) ─────────────────────────────────────────────────────────

fn aspect_authorize_before(ctx: String, required_role: String) -> String {
    let user_id: String = ctx_get_meta(ctx, "user_id")
    if str_eq(user_id, "") {
        return ctx_with_meta(ctx, "error", AOP_ERR_UNAUTHENTICATED)
    }
    let roles_json: String = engram_edge_traverse(user_id, "has_role")
    if !str_contains(roles_json, "\"" + required_role + "\"") {
        return ctx_with_meta(ctx, "error", AOP_ERR_FORBIDDEN)
    }
    ctx
}

// ── @cache(ttl_seconds) ──────────────────────────────────────────────────────

fn aspect_cache_around(ctx: String, ttl: Int, proceed: String) -> String {
    let key: String = ctx_get_meta(ctx, "cache_key")
    if str_eq(key, "") {
        let target: String = json_get(ctx, "target")
        let method: String = json_get(ctx, "method")
        let args:   String = json_get(ctx, "args")
        let key = sha256_hex(target + ":" + method + ":" + args)
    }
    let cached: String = cache_get(key)
    if !str_eq(cached, "") {
        return ctx_with_meta(ctx, "result", cached)
    }
    // Caller invokes proceed(ctx) externally; we record the key for `after` to use.
    ctx_with_meta(ctx, "cache_key", key)
}

fn aspect_cache_after(ctx: String, result: String, ttl: Int) -> String {
    let key: String = ctx_get_meta(ctx, "cache_key")
    if !str_eq(key, "") { cache_put(key, result, ttl) }
    result
}

// ── @rate_limit(requests, per_seconds) ───────────────────────────────────────

fn aspect_rate_limit_before(ctx: String, requests: Int, per_seconds: Int) -> String {
    let principal: String = ctx_get_meta(ctx, "user_id")
    if str_eq(principal, "") { let principal = ctx_get_meta(ctx, "ip") }
    let bucket_key: String = "rl:" + json_get(ctx, "target") + ":" + principal
    let allowed: Bool = rate_bucket_take(bucket_key, requests, per_seconds)
    if !allowed { return ctx_with_meta(ctx, "error", AOP_ERR_RATE_LIMITED) }
    ctx
}

// ── @retry(attempts, backoff_ms) ────────────────────────────────────────────
//
// retry is necessarily an `around` aspect — it must own the loop.

fn aspect_retry_around(ctx: String, attempts: Int, backoff_ms: Int, proceed_fn_name: String) -> String {
    let i: Int = 0
    let result: String = ""
    while i < attempts {
        let result = call_dynamic(proceed_fn_name, ctx)
        let err: String = json_get(result, "error")
        if str_eq(err, "") { return result }
        sleep_ms(backoff_ms * (i + 1))
        let i = i + 1
    }
    ctx_with_meta(ctx, "error", AOP_ERR_RETRIES_EXHAUSTED)
}

// ── @log / @trace ────────────────────────────────────────────────────────────

fn aspect_log_before(ctx: String) -> String {
    println("[aop] -> " + json_get(ctx, "target") + "." + json_get(ctx, "method"))
    ctx
}

fn aspect_log_after(ctx: String, result: String) -> String {
    println("[aop] <- " + json_get(ctx, "target") + "." + json_get(ctx, "method"))
    result
}

fn aspect_trace_before(ctx: String) -> String {
    let span_id: String = uuid_v4()
    ctx_with_meta(ctx, "span_id", span_id)
}

// ── @validate(schema) ────────────────────────────────────────────────────────

fn aspect_validate_before(ctx: String, schema_json: String) -> String {
    let args: String = json_get(ctx, "args")
    let valid: Bool  = json_schema_check(args, schema_json)
    if !valid { return ctx_with_meta(ctx, "error", AOP_ERR_VALIDATION) }
    ctx
}

// ── Aspect chain composition ─────────────────────────────────────────────────
//
// The compiler emits a call sequence like:
//   ctx = ctx_new(...)
//   ctx = aspect_authenticate_before(ctx)
//   ctx = aspect_log_before(ctx)
//   result = proceed(ctx)
//   result = aspect_log_after(ctx, result)
// At runtime an explicit `chain_run` exists for dynamic composition.

fn chain_run(ctx: String, before_fns: String, around_fn: String, after_fns: String, proceed_fn: String) -> String {
    let cur_ctx: String = ctx
    // before chain
    let i: Int = 0
    let befs: String = before_fns
    while !str_eq(befs, "") {
        let comma: Int = str_index_of(befs, ",")
        let fn_name: String = befs
        if comma > 0 { let fn_name = str_slice(befs, 0, comma) }
        let cur_ctx = call_dynamic(fn_name, cur_ctx)
        let err: String = ctx_get_meta(cur_ctx, "error")
        if !str_eq(err, "") { return cur_ctx }
        if comma > 0 { let befs = str_slice(befs, comma + 1, str_len(befs)) }
        if comma < 0 { let befs = "" }
    }
    // around / proceed
    let result: String = call_dynamic(proceed_fn, cur_ctx)
    // after chain (right-to-left composition; simplified left-to-right here)
    let afts: String = after_fns
    while !str_eq(afts, "") {
        let comma: Int = str_index_of(afts, ",")
        let fn_name: String = afts
        if comma > 0 { let fn_name = str_slice(afts, 0, comma) }
        let result = call_dynamic2(fn_name, cur_ctx, result)
        if comma > 0 { let afts = str_slice(afts, comma + 1, str_len(afts)) }
        if comma < 0 { let afts = "" }
    }
    result
}

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

let ctx: String = ctx_new("ProfilePage", "render")
let ctx = ctx_with_arg(ctx, "user_id", "u-001")
println("[el-aop] ctx = " + ctx)
