// el-services — Pluggable service bindings.
//
// A "service" is a logical interface. A "binding" is the wire protocol that
// fulfills it. The binding kind is set in manifest.el; the service code does
// not change when the binding changes.
//
//   binding = "rest"        -> RestBinding
//   binding = "grpc"        -> GrpcBinding (planned)
//   binding = "websocket"   -> WebSocketBinding
//   binding = "direct"      -> in-process function call (for testing)

// ── Binding kinds ────────────────────────────────────────────────────────────

let BIND_REST:      String = "rest"
let BIND_GRPC:      String = "grpc"
let BIND_WEBSOCKET: String = "websocket"
let BIND_DIRECT:    String = "direct"

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

let SVC_ERR_NOT_FOUND:        String = "service.not_found"
let SVC_ERR_BINDING:          String = "service.binding"
let SVC_ERR_TRANSPORT:        String = "service.transport"
let SVC_ERR_AUTH:             String = "service.auth"
let SVC_ERR_METHOD_NOT_FOUND: String = "service.method_not_found"

// ── Service config ───────────────────────────────────────────────────────────

let AUTH_NONE:   String = "none"
let AUTH_BEARER: String = "bearer"
let AUTH_BASIC:  String = "basic"
let AUTH_HMAC:   String = "hmac"

type AuthConfig {
    kind:        String   // none | bearer | basic | hmac
    secret_ref:  String   // e.g. "env:API_TOKEN" or "vault:path"
}

type ServiceConfig {
    name:     String
    binding:  String
    base_url: String
    auth:     AuthConfig
    timeout_ms: Int
}

fn service_config_rest(name: String, base_url: String, auth_secret_ref: String) -> ServiceConfig {
    let auth: AuthConfig = { "kind": "bearer", "secret_ref": auth_secret_ref }
    { "name": name, "binding": "rest", "base_url": base_url, "auth": auth, "timeout_ms": 5000 }
}

// ── Service request/response ────────────────────────────────────────────────

type ServiceRequest {
    method:   String   // logical method name (e.g. "list_users")
    args:     String   // JSON
    metadata: String   // JSON (auth tokens, trace ids)
}

type ServiceResponse {
    status:  Int       // 0 = ok, non-zero = error code
    body:    String    // JSON
    error:   String    // empty if ok
}

fn response_ok(body: String) -> ServiceResponse {
    { "status": 0, "body": body, "error": "" }
}

fn response_err(code: Int, error: String) -> ServiceResponse {
    { "status": code, "body": "", "error": error }
}

// ── REST binding ────────────────────────────────────────────────────────────
//
// Method name -> (HTTP verb, path template). The compiler emits this table
// from the service interface declaration.

fn rest_invoke(cfg: ServiceConfig, http_method: String, path: String, req: ServiceRequest) -> ServiceResponse {
    let url: String = cfg.base_url + path
    let headers: String = build_auth_header(cfg.auth)
    let body: String = req.args
    let resp: String = ""
    if str_eq(http_method, "GET") {
        let resp = http_get_with_headers(url, headers)
    }
    if str_eq(http_method, "POST") {
        let resp = http_post_with_headers(url, body, headers)
    }
    if str_eq(http_method, "PUT") {
        let resp = http_put_with_headers(url, body, headers)
    }
    if str_eq(http_method, "DELETE") {
        let resp = http_delete_with_headers(url, headers)
    }
    if str_eq(resp, "") { return response_err(599, SVC_ERR_TRANSPORT) }
    response_ok(resp)
}

fn build_auth_header(auth: AuthConfig) -> String {
    if str_eq(auth.kind, "bearer") {
        let secret: String = resolve_secret_ref(auth.secret_ref)
        return "Authorization: Bearer " + secret
    }
    if str_eq(auth.kind, "basic") {
        let creds: String = resolve_secret_ref(auth.secret_ref)
        return "Authorization: Basic " + base64_encode(creds)
    }
    ""
}

fn resolve_secret_ref(ref: String) -> String {
    if str_starts_with(ref, "env:") {
        return env(str_slice(ref, 4, str_len(ref)))
    }
    if str_starts_with(ref, "vault:") {
        return vault_kv_get(str_slice(ref, 6, str_len(ref)))
    }
    ""
}

// ── gRPC binding (planned) ──────────────────────────────────────────────────
//
// Requires generated stubs from a .proto. The shape is identical to REST:
// a method dispatcher that converts logical method calls to wire calls.

fn grpc_invoke(cfg: ServiceConfig, method: String, req: ServiceRequest) -> ServiceResponse {
    response_err(501, "grpc binding not yet implemented")
}

// ── WebSocket binding ───────────────────────────────────────────────────────

fn ws_invoke(cfg: ServiceConfig, method: String, req: ServiceRequest) -> ServiceResponse {
    let frame: String = "{\"method\":\"" + method + "\",\"args\":" + req.args + "}"
    let reply: String = ws_send_recv(cfg.base_url, frame)
    if str_eq(reply, "") { return response_err(599, SVC_ERR_TRANSPORT) }
    response_ok(reply)
}

// ── Direct binding (in-process — used in tests / single-process apps) ──────

fn direct_invoke(handler_fn_name: String, req: ServiceRequest) -> ServiceResponse {
    let body: String = call_dynamic2(handler_fn_name, req.method, req.args)
    response_ok(body)
}

// ── ServiceRegistry ─────────────────────────────────────────────────────────
//
// Maps service name -> ServiceConfig. Methods are looked up in a separate
// per-service method table (logical method -> (verb, path) for REST, etc).

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

fn registry_register(reg: String, cfg: ServiceConfig) -> String {
    json_set(reg, cfg.name, json_encode(cfg))
}

fn registry_lookup(reg: String, name: String) -> String {
    json_get(reg, name)
}

// ── ServiceProxy — the user-facing call site ────────────────────────────────
//
// A ServiceProxy is a (registry, service_name, method_table) trio. The
// el-ui-compiler emits one proxy class per service interface.

type ServiceProxy {
    service_name:  String
    config:        ServiceConfig
    method_table:  String   // JSON: method -> { http_verb, path }
}

fn proxy_invoke(proxy: ServiceProxy, method: String, args_json: String) -> ServiceResponse {
    let req: ServiceRequest = { "method": method, "args": args_json, "metadata": "{}" }
    let cfg: ServiceConfig  = proxy.config
    if str_eq(cfg.binding, "rest") {
        let m: String = json_get(proxy.method_table, method)
        if str_eq(m, "") { return response_err(404, SVC_ERR_METHOD_NOT_FOUND) }
        let verb: String = json_get(m, "verb")
        let path: String = json_get(m, "path")
        return rest_invoke(cfg, verb, path, req)
    }
    if str_eq(cfg.binding, "grpc") {
        return grpc_invoke(cfg, method, req)
    }
    if str_eq(cfg.binding, "websocket") {
        return ws_invoke(cfg, method, req)
    }
    response_err(400, SVC_ERR_BINDING)
}

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

let cfg: ServiceConfig = service_config_rest("UserService", "https://api.example.com", "env:API_TOKEN")
println("[el-services] registered " + cfg.name + " @ " + cfg.base_url)
