#define FCGI_BEGIN_REQUEST       1
#define FCGI_ABORT_REQUEST       2
#define FCGI_END_REQUEST         3
#define FCGI_PARAMS              4
#define FCGI_STDIN               5
#define FCGI_STDOUT              6
#define FCGI_STDERR              7
#define FCGI_DATA                8
#define FCGI_GET_VALUES          9
#define FCGI_GET_VALUES_RESULT  10
#define FCGI_UNKNOWN_TYPE       11

#define FUFE_OK     0
#define FUFE_EOF   -1  /* unexpected protocol-level EOF */
#define FUFE_IO    -2
#define FUFE_PROTO -3
#define FUFE_PLEN  -4
#define FUFE_CLEN  -5
#define FUFE_ABORT -6  /* explicit abort or client-level EOF */
#define FUFE_NOREQ -7  /* protocol-level EOF before we received anything */

#define FUFCGI_MAX_DATA 65535

typedef struct {
    SV *self;
    int fd;
    int maxproc;
    int keepconn;

    int reqid;
    HV *headers;
    HV *params;

    /* Single buffer for reading & writing, we only do one thing at a time */
    char buf[8 + FUFCGI_MAX_DATA + 255]; /* fits a maximum-length fcgi record */
    int len; /* total number of bytes in the buffer */
    int off; /* number of bytes consumed */
} fufcgi;

typedef struct {
    unsigned char type;
    unsigned short id;
    int len;
    char *data;
} fufcgi_rec;


/* Incremental param length & name parser */
typedef enum {
    FUFC_INIT, FUFC_L1, FUFC_L2, FUFC_L3,
    FUFC_V0, FUFC_V1, FUFC_V2, FUFC_V3,
    FUFC_N0, FUFC_NX
} fufcgi_paramstate;

typedef struct {
    int namelen;
    int vallen;
    int state;
    int namerd;
    char *name;
    char namebuf[128]; /* We don't support longer param names */
} fufcgi_param;

/* Returns NULL on error or ptr to value (or 'end' if !done) */
static char *fufcgi_param_parse(fufcgi_param *p, char *buf, char *end) {
    while (buf < end) {
        switch (p->state) {
            case FUFC_INIT:
                p->vallen = p->namerd = 0;
                if (*buf & 0x80) {
                    p->namelen = (*buf & 0x1f) << 24;
                    p->state = FUFC_L1;
                } else {
                    p->namelen = *buf;
                    p->state = FUFC_V0;
                }
                break;
            case FUFC_L1:
                p->namelen |= ((unsigned char)*buf) << 16;
                p->state = FUFC_L2;
                break;
            case FUFC_L2:
                p->namelen |= ((unsigned char)*buf) << 8;
                p->state = FUFC_L3;
                break;
            case FUFC_L3:
                p->namelen |= (unsigned char)*buf;
                p->state = FUFC_V0;
                if (p->namelen > (int)sizeof(p->namebuf)) return NULL;
                break;
            case FUFC_V0:
                if (*buf & 0x80) {
                    p->vallen = (*buf & 0x1f) << 24;
                    p->state = FUFC_V1;
                } else {
                    p->vallen = *buf;
                    p->state = p->namelen ? FUFC_N0 : FUFC_INIT;
                }
                break;
            case FUFC_V1:
                p->vallen |= ((unsigned char)*buf) << 16;
                if (p->vallen) return NULL; /* Let's just disallow param values > 64 KiB */
                p->state = FUFC_V2;
                break;
            case FUFC_V2:
                p->vallen |= ((unsigned char)*buf) << 8;
                p->state = FUFC_V3;
                break;
            case FUFC_V3:
                p->vallen |= (unsigned char)*buf;
                p->state = FUFC_N0;
                break;
            case FUFC_N0:
                if (p->namelen <= end - buf) {
                    p->name = buf;
                    p->state = FUFC_INIT;
                    return buf + p->namelen;
                } else {
                    p->name = p->namebuf;
                    p->name[0] = *buf;
                    p->namerd = 1;
                    p->state = FUFC_NX;
                }
                break;
            case FUFC_NX:
                p->name[p->namerd++] = *buf;
                if (p->namerd == p->namelen) {
                    p->state = FUFC_INIT;
                    return buf + 1;
                }
                break;
        }
        buf++;
    }
    return buf;
}

static int fufcgi_fill(fufcgi *ctx, int len) {
    if ((int)sizeof(ctx->buf) - ctx->off < len) {
        memmove(ctx->buf, ctx->buf+ctx->off, ctx->len - ctx->off);
        ctx->len -= ctx->off;
        ctx->off = 0;
    }
    while (ctx->len - ctx->off < len) {
        ssize_t r = read(ctx->fd, ctx->buf+ctx->len, sizeof(ctx->buf) - ctx->len);
        if (r <= 0) return r == 0 ? FUFE_EOF : FUFE_IO;
        ctx->len += r;
    }
    return FUFE_OK;
}

static int fufcgi_read_record(fufcgi *ctx, fufcgi_rec *rec) {
    int r;
    if ((r = fufcgi_fill(ctx, 8)) != FUFE_OK) return r;

    if (ctx->buf[ctx->off] != 1) return FUFE_PROTO; /* version */
    rec->type = ctx->buf[ctx->off+1];
    rec->id = fu_frombeU(16, ctx->buf+ctx->off+2);
    rec->len = fu_frombeU(16, ctx->buf+ctx->off+4);
    int pad = (unsigned char)ctx->buf[ctx->off+6];
    ctx->off += 8;

    if ((r = fufcgi_fill(ctx, rec->len + pad)) != FUFE_OK) return r;
    rec->data = ctx->buf + ctx->off;
    ctx->off += rec->len + pad;
    return FUFE_OK;
}

/* Unbuffered write of a single record, first 8 bytes of 'buf' are filled out
 * by this function, record contents must come after. */
static int fufcgi_write_record(fufcgi *ctx, fufcgi_rec *hdr, char *buf) {
    buf[0] = 1;
    buf[1] = hdr->type;
    fu_tobeU(16, buf+2, hdr->id);
    fu_tobeU(16, buf+4, hdr->len);
    buf[6] = 0;
    buf[7] = 0;
    int len = hdr->len + 8;
    while (len > 0) {
        int r = write(ctx->fd, buf, len);
        if (r <= 0) return r == 0 ? FUFE_EOF : FUFE_IO;
        buf += r;
        len -= r;
    }
    return FUFE_OK;
}

static int fufcgi_handle_values(fufcgi *ctx, fufcgi_rec *rec, char *buf) {
    int reslen = 8;
    char *param = rec->data;
    char *end = rec->data + rec->len;
    fufcgi_param p;
    p.state = FUFC_INIT;

    while (param < end) {
        if ((param = fufcgi_param_parse(&p, param, end)) == NULL) return FUFE_PLEN;
        if (p.state != FUFC_INIT) return FUFE_PROTO;
        if (p.vallen > end - param) return FUFE_PROTO;
        if (reslen >= 100) return FUFE_PROTO; /* implies requested params were duplicated */

        if (p.namelen == 14 && memcmp(p.name, "FCGI_MAX_CONNS", 14) == 0) {
            memcpy(buf+reslen, "\x0e\0FCGI_MAX_CONNS", 16);
            int l = sprintf(buf+reslen+16, "%d", ctx->maxproc);
            buf[reslen+1] = l;
            reslen += 16 + l;

        } else if (p.namelen == 13 && memcmp(p.name, "FCGI_MAX_REQS", 13) == 0) {
            memcpy(buf+reslen, "\x0d\0FCGI_MAX_REQS", 15);
            int l = sprintf(buf+reslen+15, "%d", ctx->maxproc);
            buf[reslen+1] = l;
            reslen += 15 + l;

        } else if (p.namelen == 15 && memcmp(p.name, "FCGI_MPXS_CONNS", 15) == 0) {
            memcpy(buf+reslen, "\x0f\1FCGI_MPXS_CONNS0", 18);
            reslen += 18;
        }

        param += p.vallen;
    }
    rec->type = FCGI_GET_VALUES_RESULT;
    rec->len = reslen - 8;
    return fufcgi_write_record(ctx, rec, buf);
}

/* Read a PARAMS/STDIN/ABORT record corresponding to the current id, starts
 * reading a new request if id=0. */
static int fufcgi_read_req_record(fufcgi *ctx, fufcgi_rec *rec) {
    int r;
    char tmp[128]; /* Large enough for a FCGI_GET_VALUES_RESULT */
    while (1) {
        if ((r = fufcgi_read_record(ctx, rec)) != FUFE_OK) return r == FUFE_EOF && ctx->len == 0 ? FUFE_NOREQ : r;

        switch (rec->type) {
            case FCGI_PARAMS:
            case FCGI_STDIN:
            case FCGI_ABORT_REQUEST:
                if (rec->id != ctx->reqid) return FUFE_PROTO;
                return FUFE_OK;
            case FCGI_BEGIN_REQUEST:
                if (!rec->id || rec->id == ctx->reqid) return FUFE_PROTO;
                if (rec->len != 8) return FUFE_PROTO;
                ctx->keepconn = rec->data[2] & 1;
                if (rec->data[0] != 0 || rec->data[1] != 1) { /* FCGI_RESPONDER */
                    memcpy(tmp+8, "\0\0\0\0\3\0\0\0", 8); /* FCGI_UNKNOWN_ROLE */
                    rec->type = FCGI_END_REQUEST;
                    rec->len = 8;
                    if ((r = fufcgi_write_record(ctx, rec, tmp)) != FUFE_OK) return r;
                    if (!ctx->keepconn) return FUFE_EOF;
                } else if (ctx->reqid) {
                    memcpy(tmp+8, "\0\0\0\0\1\0\0\0", 8); /* FCGI_CANT_MPX_CONN */
                    rec->type = FCGI_END_REQUEST;
                    rec->len = 8;
                    if ((r = fufcgi_write_record(ctx, rec, tmp)) != FUFE_OK) return r;
                    if (!ctx->keepconn) return FUFE_EOF;
                } else {
                    ctx->reqid = rec->id;
                }
                break;
            case FCGI_GET_VALUES:
                if (rec->id) return FUFE_PROTO;
                if ((r = fufcgi_handle_values(ctx, rec, tmp)) != FUFE_OK) return r;
                break;
            default:
                memset(tmp+8, 0, 8);
                tmp[8] = rec->type;
                rec->type = FCGI_UNKNOWN_TYPE;
                rec->len = 8;
                rec->id = 0;
                if ((r = fufcgi_write_record(ctx, rec, tmp)) != FUFE_OK) return r;
                break;
        }
    }
}

static int fufcgi_read_params(pTHX_ fufcgi *ctx, fufcgi_rec *rec) {
    int r;
    fufcgi_param p;
    p.state = FUFC_INIT;

    SV *valsv = NULL;
    char *val = NULL;
    int valleft = 0;

    while (1) {
        if ((r = fufcgi_read_req_record(ctx, rec)) != FUFE_OK) return r;
        if (rec->type == FCGI_ABORT_REQUEST) return FUFE_OK;
        if (rec->type != FCGI_PARAMS) return FUFE_PROTO;
        if (rec->len == 0) return p.state != FUFC_INIT || valleft ? FUFE_PROTO : FUFE_OK;

        char *buf = rec->data;
        char *end = rec->data + rec->len;
        while (buf < end) {
            if (valleft) {
                r = valleft > end - buf ? end - buf : valleft;
                if (val) {
                    memcpy(val, buf, r);
                    val += r;
                }
                valleft -= r;
                buf += r;
                if (val && !valleft) {
                    *val = 0;
                    SvCUR_set(valsv, p.vallen);
                }
                continue;
            }
            if ((buf = fufcgi_param_parse(&p, buf, end)) == NULL) return FUFE_PLEN;
            if (p.state != FUFC_INIT) break;

            valsv = NULL;
            val = NULL;
            valleft = p.vallen;

            /* https://www.rfc-editor.org/rfc/rfc3875 */

            /* Request header */
            if (p.namelen > 5 && memcmp(p.name, "HTTP_", 5) == 0) {
                p.namelen -= 5;
                p.name += 5;
                for (r=0; r<p.namelen; r++)
                    p.name[r] = p.name[r] == '_' ? '-' : p.name[r] >= 'A' && p.name[r] <= 'Z' ? p.name[r] | 0x20 : p.name[r];
                if (!(p.namelen == 14 && memcmp(p.name, "content-length", 14) == 0)
                        && !(p.namelen == 12 && memcmp(p.name, "content-type", 12) == 0)) {
                    valsv = newSV(p.vallen+1);
                    hv_store(ctx->headers, p.name, p.namelen, valsv, 0);
                }

            } else if (p.namelen == 14 && memcmp(p.name, "CONTENT_LENGTH", 14) == 0) {
                valsv = newSV(p.vallen+1);
                hv_stores(ctx->headers, "content-length", valsv);

            } else if (p.namelen == 12 && memcmp(p.name, "CONTENT_TYPE", 12) == 0) {
                valsv = newSV(p.vallen+1);
                hv_stores(ctx->headers, "content-type", valsv);

            } else if (p.namelen == 11 && memcmp(p.name, "REMOTE_ADDR", 11) == 0) {
                valsv = newSV(p.vallen+1);
                hv_stores(ctx->params, "ip", valsv);

            } else if (p.namelen == 12 && memcmp(p.name, "QUERY_STRING", 12) == 0) {
                valsv = newSV(p.vallen+1);
                hv_stores(ctx->params, "qs", valsv);

            } else if (p.namelen == 14 && memcmp(p.name, "REQUEST_METHOD", 14) == 0) {
                valsv = newSV(p.vallen+1);
                hv_stores(ctx->params, "method", valsv);

            /* Not in rfc3875; there's no standardized parameter for the URI,
             * but every FastCGI-capable web server includes this one */
            } else if (p.namelen == 11 && memcmp(p.name, "REQUEST_URI", 11) == 0) {
                valsv = newSV(p.vallen+1);
                hv_stores(ctx->params, "path", valsv);

            } else { /* ignore */ }

            if (valsv) {
                SvPOK_only(valsv);
                val = SvPVX(valsv);
                *val = 0; /* in case vallen = 0 */
            }
        }
    }
}

static int fufcgi_read_req(pTHX_ fufcgi *ctx, SV *headers, SV *params) {
    if (ctx->reqid) fu_confess("Invalid attempt to read FastCGI request before finishing the previous one");
    fufcgi_rec rec;
    int r;

    ctx->off = ctx->len = 0;
    ctx->headers = (HV *)SvRV(headers);
    ctx->params = (HV *)SvRV(params);
    if ((r = fufcgi_read_params(aTHX_ ctx, &rec)) != FUFE_OK) return r;

    int stdinlen = 0;
    SV **contentlength = hv_fetchs(ctx->headers, "content-length", 0);
    if (contentlength && *contentlength) {
        UV uv = 0;
        char *v = SvPV_nolen(*contentlength);
        if (*v && !grok_atoUV(v, &uv, NULL)) return FUFE_CLEN;
        if (uv >= INT_MAX) return FUFE_CLEN;
        stdinlen = uv;
    }

    SV *sv = newSV(stdinlen+1);
    hv_stores(ctx->params, "body", sv);
    SvPOK_only(sv);
    char *stdinbuf = SvPVX(sv);
    int stdinleft = stdinlen;

    while (1) {
        if (rec.type == FCGI_ABORT_REQUEST) return FUFE_ABORT;
        else if (rec.type == FCGI_PARAMS) {
            if (rec.len != 0) return FUFE_PROTO;
        } else if (rec.type == FCGI_STDIN) {
            if (rec.len == 0) {
                *stdinbuf = 0;
                SvCUR_set(sv, stdinlen - stdinleft);
                return stdinleft == 0 ? FUFE_OK : FUFE_ABORT;
            }
            if (rec.len > stdinleft) return FUFE_PROTO;
            memcpy(stdinbuf, rec.data, rec.len);
            stdinbuf += rec.len;
            stdinleft -= rec.len;
        } else {
            return FUFE_PROTO;
        }
        if ((r = fufcgi_read_req_record(ctx, &rec)) != FUFE_OK) return r;
    }
}

static void fufcgi_flush(fufcgi *ctx) {
    fufcgi_rec hdr;
    if (ctx->len > 0) {
        hdr.len = ctx->len;
        hdr.type = FCGI_STDOUT;
        hdr.id = ctx->reqid;
        fufcgi_write_record(ctx, &hdr, ctx->buf);
        ctx->len = 0;
    }
}

static void fufcgi_print(fufcgi *ctx, const char *buf, int len) {
    int r;
    while (len > 0) {
        r = len > FUFCGI_MAX_DATA - ctx->len ? FUFCGI_MAX_DATA - ctx->len : len;
        memcpy(ctx->buf+8+ctx->len, buf, r);
        ctx->len += r;
        len -= r;
        buf += r;
        if (ctx->len >= FUFCGI_MAX_DATA) fufcgi_flush(ctx);
    }
}

static void fufcgi_done(fufcgi *ctx) {
    fufcgi_rec hdr;
    fufcgi_flush(ctx);

    hdr.len = 0;
    hdr.type = FCGI_STDOUT;
    hdr.id = ctx->reqid;
    fufcgi_write_record(ctx, &hdr, ctx->buf);

    memcpy(ctx->buf+8, "\0\0\0\0\0\0\0\0", 8); /* FCGI_REQUEST_COMPLETE */
    hdr.type = FCGI_END_REQUEST;
    hdr.len = 8;
    fufcgi_write_record(ctx, &hdr, ctx->buf);

    ctx->reqid = ctx->len = ctx->off = 0;
}
