/*
  This file is part of Paivana.
  Copyright (C) 2026 Taler Systems SA

  Paivana is free software; you can redistribute it and/or
  modify it under the terms of the GNU Affero General Public License
  as published by the Free Software Foundation; either version
  3, or (at your option) any later version.

  Paivana is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public
  License along with Paivana; see the file COPYING.  If not,
  write to the Free Software Foundation, Inc., 51 Franklin
  Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

/**
 * @file pipeline_client.c
 * @brief HTTP/1.1 pipelining client: opens a single TCP connection
 *        to a host/port, sends N GET requests back-to-back *without*
 *        waiting for their responses, then reads N responses in
 *        order and prints each body (separated by "---").
 *
 * Used by the paivana test suite to verify that paivana correctly
 * handles pipelined requests on a single keep-alive connection.
 */
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#define BUF_GROW 8192

static int
connect_to (const char *host,
            const char *port)
{
  struct addrinfo hints = { 0 };
  struct addrinfo *res = NULL;
  int rc;

  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  rc = getaddrinfo (host,
                    port,
                    &hints,
                    &res);
  if (0 != rc)
  {
    fprintf (stderr,
             "getaddrinfo(%s:%s): %s\n",
             host,
             port,
             gai_strerror (rc));
    return -1;
  }
  for (struct addrinfo *p = res; NULL != p; p = p->ai_next)
  {
    int fd = socket (p->ai_family,
                     p->ai_socktype,
                     p->ai_protocol);
    if (fd < 0)
      continue;
    if (0 == connect (fd,
                      p->ai_addr,
                      p->ai_addrlen))
    {
      freeaddrinfo (res);
      return fd;
    }
    close (fd);
  }
  freeaddrinfo (res);
  fprintf (stderr,
           "could not connect to %s:%s\n",
           host,
           port);
  return -1;
}


static int
write_all (int fd,
           const char *buf,
           size_t len)
{
  while (len > 0)
  {
    ssize_t w = write (fd,
                       buf,
                       len);

    if (w < 0)
    {
      if (EINTR == errno)
        continue;
      return -1;
    }
    buf += w;
    len -= (size_t) w;
  }
  return 0;
}


/**
 * Read exactly @a want bytes from @a fd into the tail of the buffer.
 * Grows the buffer as needed.  @a have is updated.
 */
static int
ensure_bytes (int fd,
              char **buf,
              size_t *cap,
              size_t *have,
              size_t want)
{
  while (*have < want)
  {
    ssize_t r;
    if (*have + BUF_GROW > *cap)
    {
      size_t nc = *cap ? *cap * 2 : BUF_GROW;
      char *nb;

      while (nc < *have + BUF_GROW)
        nc *= 2;
      nb = realloc (*buf,
                    nc);
      if (NULL == nb)
        return -1;
      *buf = nb;
      *cap = nc;
    }

    r = read (fd,
              *buf + *have,
              *cap - *have);
    if (r < 0)
    {
      if (EINTR == errno)
        continue;
      return -1;
    }
    if (0 == r)
      return -1; /* EOF */
    *have += (size_t) r;
  }
  return 0;
}


/**
 * Find the end of the HTTP header block ("\r\n\r\n") starting at
 * some offset in buf.  Returns the absolute offset of the byte
 * just past the final "\r\n\r\n", or (size_t) -1 if not yet present.
 * Grows the buffer / reads more data as needed.
 */
static size_t
read_headers (int fd,
              char **buf,
              size_t *cap,
              size_t *have,
              size_t start)
{
  /* First offset not yet examined.  Carried across the read loop:
     restarting the scan at @a start after every read would be
     quadratic in the size of the header block. */
  size_t scan = start;

  while (1)
  {
    while (scan + 3 < *have)
    {
      if ( ('\r' == (*buf)[scan]) &&
           ('\n' == (*buf)[scan + 1]) &&
           ('\r' == (*buf)[scan + 2]) &&
           ('\n' == (*buf)[scan + 3]) )
        return scan + 4;
      scan++;
    }
    if (ensure_bytes (fd,
                      buf,
                      cap,
                      have,
                      *have + 1))
      return (size_t) -1;
  }
}


/**
 * Extract Content-Length from a header block [start, hdr_end).
 * Returns -1 if not found.
 */
static long long
header_content_length (const char *buf,
                       size_t start,
                       size_t hdr_end)
{
  const char *hdrs = buf + start;
  size_t len = hdr_end - start;
  const char *p = hdrs;
  const char *end = hdrs + len;
  const char *needle = "Content-Length:";

  while (p + strlen (needle) < end)
  {
    const char *line_end = memchr (p,
                                   '\n',
                                   end - p);

    if (NULL == line_end)
      break;
    if (0 == strncasecmp (p,
                          needle,
                          strlen (needle)))
    {
      const char *v = p + strlen (needle);

      while ( (v < line_end) &&
              (' ' == *v ||
               '\t' == *v) )
        v++;
      return strtoll (v,
                      NULL,
                      10);
    }
    p = line_end + 1;
  }
  return -1;
}


/**
 * Parse the status code out of "HTTP/1.1 <code> ..."
 */
static int
header_status (const char *buf,
               size_t start,
               size_t hdr_end)
{
  const char *p = buf + start;
  const char *sp = memchr (p,
                           ' ',
                           hdr_end - start);

  if (NULL == sp)
    return -1;
  return atoi (sp + 1);
}


int
main (int argc, char **argv)
{
  const char *host;
  const char *port;
  int n_paths;
  int fd;

  /* Before argc is checked: with argc == 1, argv[2] is a read one
     past the end of the argument vector. */
  if (argc < 4)
  {
    fprintf (stderr,
             "usage: %s <host> <port> <path> [<path>...]\n",
             argv[0]);
    return 2;
  }
  host = argv[1];
  port = argv[2];
  n_paths = argc - 3;

  fd = connect_to (host,
                   port);
  if (fd < 0)
    return 1;

  /* Pipeline: send all requests back-to-back. */
  for (int i = 0; i < n_paths; i++)
  {
    char req[2048];
    const char *conn_hdr
      = (i == n_paths - 1)
      ? "close"
      : "keep-alive";
    int n = snprintf (req,
                      sizeof (req),
                      "GET %s HTTP/1.1\r\n"
                      "Host: %s:%s\r\n"
                      "User-Agent: paivana-pipeline-test\r\n"
                      "Connection: %s\r\n"
                      "\r\n",
                      argv[3 + i],
                      host,
                      port,
                      conn_hdr);
    if ( (n < 0) ||
         ((size_t) n >= sizeof (req)) )
    {
      fprintf (stderr,
               "request too long\n");
      close (fd);
      return 1;
    }
    if (0 != write_all (fd,
                        req,
                        (size_t) n))
    {
      fprintf (stderr,
               "write failed: %s\n",
               strerror (errno));
      close (fd);
      return 1;
    }
  }

  {
    /* Now read N responses in order, using Content-Length. */
    char *buf = NULL;
    size_t cap = 0;
    size_t have = 0;
    size_t pos = 0;
    int ret = 0;

    for (int i = 0; i < n_paths; i++)
    {
      size_t hdr_end = read_headers (fd,
                                     &buf,
                                     &cap,
                                     &have,
                                     pos);
      long status;
      long long cl;
      size_t body_end;

      if ((size_t) -1 == hdr_end)
      {
        fprintf (stderr,
                 "failed to read headers of response #%d\n",
                 i);
        ret = 1;
        break;
      }
      status = header_status (buf,
                              pos,
                              hdr_end);
      cl = header_content_length (buf,
                                  pos,
                                  hdr_end);
      if (cl < 0)
      {
        fprintf (stderr,
                 "response #%d has no Content-Length (got status %d); "
                 "cannot safely parse pipelined response stream\n",
                 i,
                 (int) status);
        ret = 1;
        break;
      }

      body_end = hdr_end + (size_t) cl;
      if (0 != ensure_bytes (fd,
                             &buf,
                             &cap,
                             &have,
                             body_end))
      {
        fprintf (stderr,
                 "short body for response #%d (expected %lld bytes)\n",
                 i, cl);
        ret = 1;
        break;
      }
      printf ("--- response %d: status=%d len=%lld ---\n",
              i,
              (int) status,
              cl);
      fwrite (buf + hdr_end,
              1,
              (size_t) cl,
              stdout);
      if ( (cl > 0) &&
           (buf[body_end - 1] != '\n') )
        putchar ('\n');
      pos = body_end;
    }
    free (buf);
    close (fd);
    return ret;
  }
}
