#ifndef OA_CLIENT_H
#define OA_CLIENT_H

/* Open::API::Client - the spec-driven HTTP client on Fetch's C ABI.
 *
 * A client is a blessed HV: { api => Open::API, base_url, validate, ua,
 * ua_opts => [k, v, ...] }. call($operationId, %params) validates the flat
 * %params against the operation's compiled JSF handles (croaking BEFORE any
 * I/O on bad input), builds the URL/headers/body in C, and fires the request
 * through the Fetch ABI. The returned Fetch::Future resolves to
 *   { status, headers => {lc-name => value}, data, error? }
 * - `data` is the JSON-decoded body when the response is application/json,
 * the raw body otherwise; transport failures resolve with status 0 and
 * `error`; with validate => 1 a response-schema mismatch sets `error` and
 * `errors`. */

/* ---- lazy Fetch ABI resolution (the jsf_fetch.h pattern) ------------------- */

static int oa_fetch_init(pTHX) {
    dSP; int count; IV p = 0;
    if (OA_FETCH) return 1;
    if (OA_FETCH_TRIED) return 0;
    OA_FETCH_TRIED = 1;
    eval_pv("require Fetch;", FALSE);
    if (SvTRUE(ERRSV)) return 0;
    ENTER; SAVETMPS; PUSHMARK(SP); PUTBACK;
    count = call_pv("Fetch::_abi_ptr", G_SCALAR | G_EVAL);
    SPAGAIN;
    if (!SvTRUE(ERRSV) && count > 0) p = POPi;
    else if (count > 0) (void)POPs;
    PUTBACK; FREETMPS; LEAVE;
    if (p) {
        const fetch_abi *f = INT2PTR(const fetch_abi *, p);
        if (f && f->abi_version == FETCH_ABI_VERSION) OA_FETCH = f;
    }
    return OA_FETCH != NULL;
}

/* ---- percent-encoding ------------------------------------------------------- */

static void oa_pct_encode_into(pTHX_ SV *out, const char *p, STRLEN l) {
    static const char hex[] = "0123456789ABCDEF";
    STRLEN i;
    for (i = 0; i < l; i++) {
        U8 c = (U8)p[i];
        if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
            || (c >= '0' && c <= '9') || c == '-' || c == '.'
            || c == '_' || c == '~') {
            sv_catpvn(out, (const char *)&c, 1);
        } else {
            char b[3];
            b[0] = '%'; b[1] = hex[c >> 4]; b[2] = hex[c & 15];
            sv_catpvn(out, b, 3);
        }
    }
}

/* ---- client-side validation -------------------------------------------------- */

/* croak with the first JSF error message for a bad client-side value */
static void oa_cli_bad(pTHX_ oa_op *o, const char *in, SV *name, SV *value,
                       SV *handle) {
    AV *errs = (AV *)sv_2mortal((SV *)newAV());
    const char *msg = "value does not match the schema";
    (void)JSF->validate(aTHX_ handle, value, errs);
    if (av_len(errs) >= 0) {
        SV **e = av_fetch(errs, 0, 0);
        HV *h = (e && *e && SvROK(*e)) ? (HV *)SvRV(*e) : NULL;
        SV *m = h ? oa_get(aTHX_ h, "message") : NULL;
        if (m) msg = SvPV_nolen(m);
    }
    croak("Open::API::Client: %s: invalid %s parameter '%s': %s",
          SvPV_nolen(o->op_id), in, name ? SvPV_nolen(name) : "body", msg);
}

/* ---- the response map callback ------------------------------------------------ */

typedef struct oa_cli_ctx {
    SV    *api_sv;      /* keeps the op table alive (+1)   */
    oa_op *op;
    int    validate;
} oa_cli_ctx;

static SV *oa_cli_map(pTHX_ int ok, int status, AV *headers, SV *body,
                      SV *err, void *ud) {
    oa_cli_ctx *c = (oa_cli_ctx *)ud;
    HV *out = newHV();
    HV *hh  = newHV();
    int is_json = 0;

    if (!ok) {
        (void)hv_stores(out, "status", newSViv(0));
        (void)hv_stores(out, "error",
            (err && SvOK(err)) ? newSVsv(err)
                               : newSVpvs("request failed"));
        SvREFCNT_dec((SV *)hh);
        goto done;
    }

    (void)hv_stores(out, "status", newSViv(status));
    {
        SSize_t i, n = headers ? av_len(headers) + 1 : 0;
        for (i = 0; i + 1 < n; i += 2) {
            SV **k = av_fetch(headers, i, 0);
            SV **v = av_fetch(headers, i + 1, 0);
            if (k && *k && v && *v) {
                STRLEN kl; const char *kp = SvPV_const(*k, kl);
                char lc[128];
                STRLEN j;
                if (kl >= sizeof lc) continue;
                for (j = 0; j < kl; j++) lc[j] = (char)toLOWER((U8)kp[j]);
                (void)hv_store(hh, lc, (I32)kl, newSVsv(*v), 0);
                if (kl == 12 && memEQ(lc, "content-type", 12)) {
                    STRLEN vl; const char *vp = SvPV_const(*v, vl);
                    if (vl >= 16 && memEQ(vp, "application/json", 16))
                        is_json = 1;
                }
            }
        }
    }
    (void)hv_stores(out, "headers", newRV_noinc((SV *)hh));

    {
        SV *data = NULL;
        if (is_json && body && SvOK(body) && SvCUR(body))
            data = oa_body_decode(aTHX_ body);   /* mortal or NULL */
        if (data) (void)hv_stores(out, "data", newSVsv(data));
        else      (void)hv_stores(out, "data",
                       body && SvOK(body) ? newSVsv(body) : newSV(0));

        /* optional response-schema validation */
        if (c->validate && c->op && data) {
            SV *handle = NULL;
            char sbuf[8];
            STRLEN sl = (STRLEN)my_snprintf(sbuf, sizeof sbuf, "%d", status);
            int i;
            for (i = 0; i < c->op->nresps; i++) {
                STRLEN rl; const char *rp = SvPV_const(c->op->resps[i].status, rl);
                if (rl == sl && memEQ(rp, sbuf, sl))
                    { handle = c->op->resps[i].handle; break; }
            }
            if (!handle) {
                for (i = 0; i < c->op->nresps; i++) {
                    STRLEN rl; const char *rp = SvPV_const(c->op->resps[i].status, rl);
                    if (rl == 7 && memEQ(rp, "default", 7))
                        { handle = c->op->resps[i].handle; break; }
                }
            }
            if (handle) {
                AV *errs = newAV();
                if (!JSF->validate(aTHX_ handle, data, errs)) {
                    (void)hv_stores(out, "error",
                                    newSVpvs("response validation failed"));
                    (void)hv_stores(out, "errors", newRV_noinc((SV *)errs));
                } else {
                    SvREFCNT_dec((SV *)errs);
                }
            }
        }
    }

done:
    if (c) { if (c->api_sv) SvREFCNT_dec(c->api_sv); Safefree(c); }
    return newRV_noinc((SV *)out);
}

/* ---- call ----------------------------------------------------------------------- */

/* the client HV field, or NULL */
static SV *oa_cli_get(pTHX_ HV *self, const char *k) {
    return oa_get(aTHX_ self, k);
}

/* lazily build (and cache) the client's Fetch UA from ua_opts */
static SV *oa_cli_ua(pTHX_ HV *self) {
    SV *ua = oa_cli_get(aTHX_ self, "ua");
    AV *opts;
    SV *kv[16];
    int nkv = 0;
    if (ua && SvROK(ua)) return ua;
    opts = oa_av_of(oa_cli_get(aTHX_ self, "ua_opts"));
    if (opts) {
        SSize_t i, n = av_len(opts) + 1;
        for (i = 0; i < n && nkv < 16; i++) {
            SV **e = av_fetch(opts, i, 0);
            kv[nkv++] = (e && *e) ? *e : &PL_sv_undef;
        }
    }
    ua = OA_FETCH->ua_new(aTHX_ kv, nkv);            /* +1 */
    (void)hv_stores(self, "ua", ua);                 /* HV takes the ref */
    return ua;
}

/* Build and fire one call. params is the flat name => value HV. Returns the
 * request future (+1). */
static SV *oa_cli_call(pTHX_ HV *self, SV *op_id, HV *params) {
    SV *api_sv = oa_cli_get(aTHX_ self, "api");
    oa_api *a;
    oa_op *o;
    SV *base = oa_cli_get(aTHX_ self, "base_url");
    SV *url;
    fetch_hdr hdrs[32];
    int nh = 0;
    SV *cookie = NULL, *bodyjson = NULL;
    const char *bp = NULL;
    STRLEN blen = 0;
    char method[16];
    STRLEN ml;
    oa_cli_ctx *ctx;
    int loc, i;

    if (!api_sv || !SvROK(api_sv))
        croak("Open::API::Client: no api on this client");
    a = (oa_api *)INT2PTR(void *, SvIV(SvRV(api_sv)));
    o = oa_op_by_id(aTHX_ a, op_id);
    if (!o) croak("Open::API::Client: unknown operationId '%s'",
                  SvPV_nolen(op_id));
    if (!oa_fetch_init(aTHX))
        croak("Open::API::Client requires Fetch with its C ABI "
              "(Fetch 0.05 or later)");

    /* ---- validate every declared parameter (fail before I/O) ---- */
    for (loc = 0; loc < OA_IN_N; loc++) {
        for (i = 0; i < o->nparams[loc]; i++) {
            oa_param *pp = &o->params[loc][i];
            STRLEN nl; const char *np = SvPV_const(pp->name, nl);
            SV **v = hv_fetch(params, np, (I32)nl, 0);
            if (v && *v && SvOK(*v)) {
                if (pp->handle && !JSF->is_valid(aTHX_ pp->handle, *v))
                    oa_cli_bad(aTHX_ o, oa_loc_name(loc), pp->name, *v,
                               pp->handle);
            } else if (pp->required) {
                croak("Open::API::Client: %s: missing required %s parameter '%.*s'",
                      SvPV_nolen(o->op_id), oa_loc_name(loc), (int)nl, np);
            }
        }
    }

    /* ---- body ---- */
    if (o->nbodies) {
        SV **bv = hv_fetchs(params, "body", 0);
        SV *bsv = (bv && *bv && SvOK(*bv)) ? *bv : NULL;
        oa_body *jb = NULL;
        for (i = 0; i < o->nbodies; i++)
            if (o->bodies[i].handle) { jb = &o->bodies[i]; break; }
        if (!bsv) {
            if (o->body_required)
                croak("Open::API::Client: %s: missing required body",
                      SvPV_nolen(o->op_id));
        } else if (jb) {
            if (!JSF->is_valid(aTHX_ jb->handle, bsv))
                oa_cli_bad(aTHX_ o, "body", NULL, bsv, jb->handle);
            bodyjson = oa_json_encode(aTHX_ bsv);    /* mortal */
            if (!bodyjson)
                croak("Open::API::Client: %s: could not encode body",
                      SvPV_nolen(o->op_id));
            bp = SvPV_const(bodyjson, blen);
            hdrs[nh].name = "Content-Type"; hdrs[nh].nlen = 12;
            hdrs[nh].val  = "application/json"; hdrs[nh].vlen = 16;
            nh++;
        }
    }

    /* ---- URL: base + interpolated path + query ---- */
    url = sv_2mortal(newSVpvs(""));
    if (base && SvOK(base)) {
        STRLEN bl; const char *bpv = SvPV_const(base, bl);
        while (bl && bpv[bl - 1] == '/') bl--;
        sv_catpvn(url, bpv, bl);
    }
    for (i = 0; i < o->nsegs; i++) {
        sv_catpvs(url, "/");
        if (o->segs[i].lit) {
            sv_catsv(url, o->segs[i].lit);
        } else {
            STRLEN nl; const char *np = SvPV_const(o->segs[i].pname, nl);
            SV **v = hv_fetch(params, np, (I32)nl, 0);
            STRLEN vl; const char *vp = SvPV_const(*v, vl);   /* required above */
            oa_pct_encode_into(aTHX_ url, vp, vl);
        }
    }
    if (!o->nsegs) sv_catpvs(url, "/");
    {
        int first = 1;
        for (i = 0; i < o->nparams[OA_IN_QUERY]; i++) {
            oa_param *pp = &o->params[OA_IN_QUERY][i];
            STRLEN nl; const char *np = SvPV_const(pp->name, nl);
            SV **v = hv_fetch(params, np, (I32)nl, 0);
            AV *multi;
            if (!v || !*v || !SvOK(*v)) continue;
            multi = oa_av_of(*v);
            if (multi) {
                SSize_t j, n = av_len(multi) + 1;
                for (j = 0; j < n; j++) {
                    SV **e = av_fetch(multi, j, 0);
                    STRLEN vl; const char *vp;
                    if (!e || !*e || !SvOK(*e)) continue;
                    sv_catpvn(url, first ? "?" : "&", 1); first = 0;
                    oa_pct_encode_into(aTHX_ url, np, nl);
                    sv_catpvs(url, "=");
                    vp = SvPV_const(*e, vl);
                    oa_pct_encode_into(aTHX_ url, vp, vl);
                }
            } else {
                STRLEN vl; const char *vp = SvPV_const(*v, vl);
                sv_catpvn(url, first ? "?" : "&", 1); first = 0;
                oa_pct_encode_into(aTHX_ url, np, nl);
                sv_catpvs(url, "=");
                oa_pct_encode_into(aTHX_ url, vp, vl);
            }
        }
    }

    /* ---- headers + cookies ---- */
    for (i = 0; i < o->nparams[OA_IN_HEADER] && nh < 31; i++) {
        oa_param *pp = &o->params[OA_IN_HEADER][i];
        STRLEN nl; const char *np = SvPV_const(pp->name, nl);
        SV **v = hv_fetch(params, np, (I32)nl, 0);
        if (v && *v && SvOK(*v)) {
            STRLEN vl; const char *vp = SvPV_const(*v, vl);
            hdrs[nh].name = np; hdrs[nh].nlen = nl;
            hdrs[nh].val  = vp; hdrs[nh].vlen = vl;
            nh++;
        }
    }
    for (i = 0; i < o->nparams[OA_IN_COOKIE]; i++) {
        oa_param *pp = &o->params[OA_IN_COOKIE][i];
        STRLEN nl; const char *np = SvPV_const(pp->name, nl);
        SV **v = hv_fetch(params, np, (I32)nl, 0);
        if (v && *v && SvOK(*v)) {
            if (!cookie) cookie = sv_2mortal(newSVpvs(""));
            else sv_catpvs(cookie, "; ");
            sv_catpvn(cookie, np, nl);
            sv_catpvs(cookie, "=");
            sv_catsv(cookie, *v);
        }
    }
    if (cookie && nh < 31) {
        STRLEN cl; const char *cp = SvPV_const(cookie, cl);
        hdrs[nh].name = "Cookie"; hdrs[nh].nlen = 6;
        hdrs[nh].val  = cp;       hdrs[nh].vlen = cl;
        nh++;
    }

    /* ---- fire ---- */
    {
        STRLEN k;
        const char *mp = SvPV_const(o->method, ml);
        if (ml >= sizeof method) ml = sizeof method - 1;
        for (k = 0; k < ml; k++) method[k] = (char)toUPPER((U8)mp[k]);
        method[ml] = '\0';
    }
    Newxz(ctx, 1, oa_cli_ctx);
    ctx->api_sv   = SvREFCNT_inc(api_sv);
    ctx->op       = o;
    ctx->validate = oa_cli_get(aTHX_ self, "validate")
                    && SvTRUE(oa_cli_get(aTHX_ self, "validate")) ? 1 : 0;
    {
        SV *ua = oa_cli_ua(aTHX_ self);
        return OA_FETCH->request(aTHX_ ua, method, SvPV_nolen(url),
                                 nh ? hdrs : NULL, nh, bp, blen,
                                 0.0, -1, oa_cli_map, ctx);
    }
}

#endif /* OA_CLIENT_H */
