/* This file is part of GNU Pies.
   Copyright (C) 2015-2026 Sergey Poznyakoff

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

   GNU Pies 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 General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with GNU Pies.  If not, see <http://www.gnu.org/licenses/>. */

#include <config.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <netdb.h>
#include <locale.h>
#include <errno.h>
#include <fcntl.h>
#include <assert.h>
#include <configmake.h>
#include <grecs.h>
#include <grecs/locus.h>
#include <grecs/json.h>
#include <wordsplit.h>
#include <sysexits.h>
#include <termios.h>
#include "base64.h"
#include "progname.h"
#include "libpies.h"
#include "grecsasrt.h"
#include "grecs.h"
#include "grecs/jsonparse.h"
#include "fprintftime.h"
#include "parseopt.h"

struct pies_url *default_url;           /* Control socket URL */
struct grecs_sockaddr *source_addr;
struct client_conn
{
  struct pies_url *url;
  struct grecs_sockaddr *source_addr;
} client;
char const *instance = "pies";
char *config_file;
char default_config_file[] = SYSCONFDIR "/piesctl.conf";
int preprocess_only;
int no_netrc_option;
int verbose;
int dump;

struct grecs_sockaddr_hints hints = { .flags = GRECS_AH_PASSIVE };

#define EX_OK            0
#define EX_PIES_NOTFOUND 1

int exit_status = EX_OK;

static void config_help (void);

static struct grecs_keyword instance_keywords[] = {
  {"socket",
   N_("url: string"),
   N_("Socket URL for that instance."),
   grecs_type_string, GRECS_DFLT,
   NULL, offsetof(struct client_conn,url), conf_callback_url},
  {"source",
   N_("arg"),
   N_("Source IP address."),
   grecs_type_sockaddr, GRECS_DFLT,
   NULL, offsetof(struct client_conn,source_addr) },
  { NULL }
};

static int
callback_instance (enum grecs_callback_command cmd,
		   grecs_node_t *node,
		   void *varptr, void *cb_data)
{
  grecs_locus_t *locus = &node->locus;
  grecs_value_t *value = node->v.value;

  switch (cmd)
    {
    case grecs_callback_section_begin:
      if (GRECS_VALUE_EMPTY_P (value))
	{
	  grecs_error (locus, 0, _("missing tag"));
	  return 1;
	}
      if (grecs_assert_value_type (value, GRECS_TYPE_STRING, locus))
	return 1;
      if (strcmp (value->v.string, instance) == 0)
	*(struct client_conn **) cb_data = &client;
      else
	*(struct client_conn **) cb_data = NULL;
      break;

    case grecs_callback_section_end:
      break;

    case grecs_callback_set_value:
      grecs_error (locus, 0, _("invalid use of block statement"));
    }
  return 0;
}

struct grecs_keyword piesctl_keywords[] = {
  {"socket",
   N_("url: string"),
   N_("Default socket URL."),
   grecs_type_string, GRECS_DFLT,
   &default_url, 0, conf_callback_url},
  {"source",
   N_("arg"),
   N_("Source IP address."),
   grecs_type_sockaddr, GRECS_DFLT,
   &source_addr },
  {"instance",
   N_("name"),
   N_("Configure connection to a pies instance"),
   grecs_type_section, GRECS_DFLT,
   NULL, 0,
   callback_instance, NULL, instance_keywords },
  { NULL }
};

static void
parse_config (void)
{
  char *file_name;
  struct grecs_node *tree;

  grecs_log_to_stderr = 1;
  grecs_preprocessor = pp_command_line ();
  grecs_sockaddr_hints = &hints;

  if (config_file)
    file_name = config_file;
  else if (access (default_config_file, F_OK) == 0)
    file_name = default_config_file;
  else
    file_name = NULL;

  if (file_name)
    {
      if (preprocess_only)
	exit (grecs_preprocess (file_name, 0)
	      ? EX_CONFIG : EX_OK);
      if (verbose)
	printf (_("%s: reading configuration from %s\n"),
		program_name, file_name);
      tree = grecs_parse (file_name);
      if (!tree)
	exit (EX_CONFIG);
      if (grecs_tree_process (tree, piesctl_keywords))
	exit (EX_CONFIG);
      if (!client.url && verbose)
	printf (_("%s: URL not found in %s\n"),
		program_name, file_name);
    }

  if (!client.url && !default_url)
    {
      /* Try local instance configuration */
      file_name = mkfilename (SYSCONFDIR, instance, ".conf");
      if (access (file_name, F_OK) == 0)
	{
	  struct grecs_node *node;

	  if (verbose)
	    printf (_("%s: reading configuration from %s\n"),
		    program_name, file_name);
	  if (preprocess_only)
	    exit (grecs_preprocess (file_name, 0)
		  ? EX_CONFIG : EX_OK);
	  tree = grecs_parse (file_name);
	  node = grecs_find_node (tree, "/control/socket");
	  if (node)
	    {
	      if (grecs_assert_value_type (node->v.value, GRECS_TYPE_STRING,
					   &node->locus))
		  exit (EX_CONFIG);

	      if (pies_url_create (&client.url, node->v.value->v.string))
		{
		  grecs_error (&node->locus, 0, _("%s: cannot create URL: %s"),
			       node->v.value->v.string, strerror (errno));
		  exit (EX_CONFIG);
		}
	    }
	  else if (verbose)
	    printf (_("%s: URL not found in %s\n"),
		    program_name, file_name);
	}
      free (file_name);
    }

  if (!client.url)
    {
      if (verbose)
	printf (_("%s: falling back to default URL\n"), program_name);
      if (default_url)
	client.url = default_url;
      else
	{
	  if (pies_url_create (&client.url, DEFAULT_PIES_CONTROL_URL))
	    {
	      grecs_error (NULL, 0, _("%s: cannot create URL: %s"),
			   DEFAULT_PIES_CONTROL_URL, strerror (errno));
	      exit (EX_SOFTWARE);
	    }
	}
    }

  if (!client.source_addr)
    client.source_addr = source_addr;

  if (verbose)
    printf (_("%s: using URL %s\n"), program_name, client.url->string);
}

static void
config_help (void)
{
  /* TRANSLATORS: do not translate words in quotes */
  static char docstring[] =
    N_("Configuration file structure for piesctl.\n"
       "For more information, use command \"info piesctl.conf\".");
  grecs_print_docstring (docstring, 0, stdout);
  grecs_print_statement_array (piesctl_keywords, 1, 0, stdout);
}

static void
piesctl_diag (grecs_locus_t const *locus, int err, int errcode,
	      const char *msg)
{
  fflush (stdout);
  fprintf (stderr, "%s: ", program_name);
  if (locus)
    {
      YY_LOCATION_PRINT (stderr, *locus);
      fputc(':', stderr);
      fputc(' ', stderr);
    }
  if (!err)
    fprintf (stderr, _("warning: "));
  fprintf (stderr, "%s", msg);
  if (errcode)
    fprintf (stderr, ": %s", strerror (errno));
  fputc ('\n', stderr);
}

enum http_method
  {
    METH_GET,
    METH_POST,
    METH_DELETE,
    METH_PUT,
    METH_OPTIONS,
    METH_INVALID
  };

char *method_names[] = {
  [METH_GET]     = "GET",
  [METH_POST]    = "POST",
  [METH_DELETE]  = "DELETE",
  [METH_PUT]     = "PUT",
  [METH_OPTIONS] = "OPTIONS",
  NULL
};

static char http_version[] = "HTTP/1.1";

struct shttp_io
{
  int code;
  struct grecs_symtab *headers;
  unsigned long content_length;
  char *content;
};

struct shttp_buf
{
  char *base;
  size_t size;
};

struct shttp_connection
{
  FILE *fp;
  struct pies_url *url;
  struct shttp_io req;
  struct shttp_io resp;
  struct shttp_buf sendbuf;
  struct shttp_buf readbuf;
  char *b64auth;
  char *status_line[3];
  struct json_value *result;
};

struct http_header
{
  char *name;
  char *value;
};

static unsigned
http_header_hash (void *data, unsigned long n_buckets)
{
  const struct http_header *p = data;
  return grecs_hash_string_ci (p->name, n_buckets);
}

static int
http_header_compare (void const *data1, void const *data2)
{
  const struct http_header *p1 = data1;
  const struct http_header *p2 = data2;
  return strcasecmp (p1->name, p2->name);
}

static int
http_header_copy (void *a, void *b)
{
  struct http_header *dst = a;
  struct http_header *src = b;

  dst->name = grecs_strdup (src->name);
  dst->value = src->value ? grecs_strdup (src->value) : NULL;

  return 0;
}

static void
http_header_free (void *p)
{
  const struct http_header *h = p;
  grecs_free (h->name);
  grecs_free (h->value);
  free (p);
}

static void
shttp_io_init (struct shttp_io *io)
{
  io->code = 0;
  io->content_length = 0;
  grecs_free (io->content);
  io->content = NULL;
  grecs_symtab_clear (io->headers);
}

static int
shttp_io_header_put (struct shttp_io *io, char const *name,
		     char const *fmt, ...)
{
  int install = 1;
  struct http_header key, *ret;
  va_list ap;
  size_t len = 0;

  if (!io->headers)
    io->headers = grecs_symtab_create (sizeof (struct http_header),
				       http_header_hash,
				       http_header_compare,
				       http_header_copy,
				       NULL,
				       http_header_free);
  key.name = (char *) name;
  key.value = NULL;
  ret = grecs_symtab_lookup_or_install (io->headers, &key, &install);
  if (!ret)
    {
      grecs_error (NULL, 0, _("cannot install header %s: %s"),
		   name, strerror (errno));
      return 1;
    }
  if (!install)
    grecs_free (ret->value);
  va_start (ap, fmt);
  grecs_vasprintf (&ret->value, &len, fmt, ap);
  va_end (ap);
  return 0;
}

static char *
shttp_io_header_get (struct shttp_io *io, char const *name)
{
  struct http_header key, *ret;

  if (!io->headers)
    return NULL;
  key.name = (char*) name;
  ret = grecs_symtab_lookup_or_install (io->headers, &key, NULL);
  return ret ? ret->value : NULL;
}

static void
shttp_io_free (struct shttp_io *io)
{
  grecs_symtab_free (io->headers);
  grecs_free (io->content);
}

static struct shttp_connection *
shttp_connect (struct pies_url *url, struct grecs_sockaddr *source_addr)
{
  int fd;
  struct pies_url *conn_url;
  struct shttp_connection *conn;
  FILE *fp;

  if (pies_url_copy (&conn_url, url))
    grecs_alloc_die ();

  if (conn_url->port == 0 && strcmp (conn_url->scheme, "inet") == 0)
    {
      conn_url->port_s = grecs_strdup ("8080");
      conn_url->port = 8080;
    }
  fd = url_connect (conn_url, source_addr);
  if (fd == -1)
    {
      pies_url_destroy (&conn_url);
      return NULL;
    }

  fp = fdopen (fd, "w+");
  if (!fp)
    {
      grecs_error (NULL, errno, _("%s: %s failed"), url->string, "fdopen");
      close (fd);
      return NULL;
    }

  conn = grecs_zalloc (sizeof (*conn));
  conn->fp = fp;
  conn->url = conn_url;

  if (!no_netrc_option)
    netrc_scan (conn->url);

  return conn;
}

static void
shttp_auth_free (struct shttp_connection *conn)
{
  if (conn->b64auth)
    {
      memset (conn->b64auth, 0, strlen (conn->b64auth));
      free (conn->b64auth);
      conn->b64auth = NULL;
    }
}

static void
shttp_status_line_free (struct shttp_connection *conn)
{
  free (conn->status_line[0]);
  free (conn->status_line[1]);
  free (conn->status_line[2]);
  conn->status_line[0] = NULL;
  conn->status_line[1] = NULL;
  conn->status_line[2] = NULL;
}

static void
shttp_close (struct shttp_connection *conn)
{
  fclose (conn->fp);
  pies_url_destroy (&conn->url);
  shttp_io_free (&conn->req);
  shttp_io_free (&conn->resp);
  grecs_free (conn->sendbuf.base);
  grecs_free (conn->readbuf.base);
  shttp_status_line_free (conn);
  shttp_auth_free (conn);
  json_value_free (conn->result);
  grecs_free (conn);
}

static void
shttp_send_line (struct shttp_connection *conn, char const *fmt, ...)
{
  va_list ap;
  va_start (ap, fmt);
  grecs_vasprintf (&conn->sendbuf.base, &conn->sendbuf.size, fmt, ap);
  va_end (ap);
  if (verbose > 1)
    printf ("> %s\n", conn->sendbuf.base);
  fprintf (conn->fp, "%s\r\n", conn->sendbuf.base);
}

static int
shttp_read_line (struct shttp_connection *conn)
{
  ssize_t rc;
  rc = grecs_getline (&conn->readbuf.base, &conn->readbuf.size, conn->fp);
  if (rc > 0)
    {
      if (conn->readbuf.base[--rc] == '\n')
	{
	  if (rc > 0 && conn->readbuf.base[rc-1] == '\r')
	    --rc;
	  conn->readbuf.base[rc] = 0;
	}
      if (verbose > 1)
	printf ("< %s\n", conn->readbuf.base);
      return 0;
    }
  else if (rc == 0)
    {
      if (verbose > 1)
	printf ("<EOF>\n");
      return 1;
    }
  grecs_error (NULL, errno, _("HTTP read error"));
  exit (EX_UNAVAILABLE);
}

static int
send_header (void *sym, void *data)
{
  struct http_header *h = sym;
  struct shttp_connection *conn = data;
  shttp_send_line (conn, "%s: %s", h->name, h->value);
  return 0;
}

static void
shttp_request_send (struct shttp_connection *conn, int method, char const *uri)
{
  shttp_send_line (conn, "%s %s %s", method_names[method], uri, http_version);
  shttp_send_line (conn, "Host: %s",
		   conn->url->host ? conn->url->host : "localhost");
  if (conn->url->user)
    {
      shttp_auth_free (conn);

      grecs_asprintf (&conn->readbuf.base, &conn->readbuf.size, "%s:%s",
		      conn->url->user,
		      conn->url->passwd ? conn->url->passwd : 0);
      base64_encode_alloc (conn->readbuf.base, strlen (conn->readbuf.base),
			   &conn->b64auth);

      pies_url_free_user (conn->url);
      pies_url_free_passwd (conn->url);
    }

  if (conn->b64auth)
    shttp_send_line (conn, "Authorization: Basic %s", conn->b64auth);

  grecs_symtab_foreach (conn->req.headers, send_header, conn);
  if (conn->req.content_length)
    {
      shttp_send_line (conn, "Content-Length: %lu", conn->req.content_length);
      shttp_send_line (conn, "Content-Type: application/json");
    }
  shttp_send_line (conn, "");
  if (conn->req.content_length)
    {
      size_t s = fwrite (conn->req.content, 1, conn->req.content_length,
			 conn->fp);
      if (s != conn->req.content_length)
	{
	  //FIXME: ngettext
	  grecs_error (NULL, 0, _("wrote %lu of %lu bytes of content"),
		       (unsigned long) s, conn->req.content_length);
	  exit (EX_UNAVAILABLE);
	}
    }
}

#define ISWS(c) ((c) == ' ' || (c) == '\t')

static void
shttp_save_header (struct shttp_connection *conn)
{
  char *p;

  for (p = conn->readbuf.base; *p; p++)
    if (*p == ':')
      {
	*p++ = 0;
	p += strspn (p, " \t");
	shttp_io_header_put (&conn->resp, conn->readbuf.base, "%s", p);
	return;
      }

  grecs_error (NULL, 0, _("malformed header line: %s"),
	       conn->readbuf.base);
  exit (EX_UNAVAILABLE);
}

static void
shttp_get_reply (struct shttp_connection *conn)
{
  enum input_state { is_initial, is_headers, is_content } state = is_initial;
  char const *val;

  shttp_io_init (&conn->resp);
  shttp_status_line_free (conn);
  json_value_free (conn->result);
  conn->result = NULL;

  while (state != is_content)
    {
      if (shttp_read_line (conn))
	break;
      if (state == is_initial)
	{
	  unsigned long n;
	  char *p;
	  int rc;

	  rc = strsplit3 (conn->readbuf.base, conn->status_line, 0);
	  if (rc == -1)
	    {
	      grecs_error (NULL, errno, "strsplit3");
	      exit (EX_OSERR);
	    }
	  else if (rc == 1)
	    {
	      grecs_error (NULL, 0, _("unexpected reply: %s"),
			   conn->readbuf.base);
	      exit (EX_UNAVAILABLE);
	    }

	  if (strcmp (conn->status_line[0], http_version))
	    {
	      grecs_error (NULL, 0, _("unsupported HTTP version: %s"),
			   conn->status_line[0]);
	      exit (EX_UNAVAILABLE);
	    }
	  n = strtoul (conn->status_line[1], &p, 0);
	  if (*p || n > 599)
	    {
	      grecs_error (NULL, 0, _("bad reply status: %s"),
			   conn->readbuf.base);
	      exit (EX_UNAVAILABLE);
	    }
	  conn->resp.code = n;

	  state = is_headers;
	}
      else if (state == is_headers)
	{
	  if (conn->readbuf.base[0])
	    shttp_save_header (conn);
	  else
	    state = is_content;
	}
    }

  val = shttp_io_header_get (&conn->resp, "content-length");
  if (val)
    {
      ssize_t rc;
      char *p;
      unsigned long len = strtoul (val, &p, 10);

      if (*p)
	{
	  grecs_error (NULL, 0, _("bad content length: %s"), val);
	  exit (EX_UNAVAILABLE);
	}

      if (len)
	{
	  conn->resp.content = grecs_malloc (len + 1);
	  conn->resp.content_length = len;
	  rc = fread (conn->resp.content, len, 1, conn->fp);
	  if (rc != 1)
	    {
	      grecs_error (NULL, ferror (conn->fp) ? errno : 0,
			   _("content read error"));
	      exit (EX_UNAVAILABLE);
	    }

	  val = shttp_io_header_get (&conn->resp, "content-type");
	  if (!val)
	    grecs_error (NULL, 0, _("no content type specified"));
	  else if (strcmp (val, "application/json"))
	    grecs_error (NULL, 0, _("unsupported content type: %s"), val);
	  else
	    {
	      struct grecs_locus errloc;
	      char const *errmsg;
	      conn->result = grecs_json_parse_string (conn->resp.content,
						      conn->resp.content_length,
						      &errmsg, &errloc);
	      if (!conn->result)
		{
		  grecs_error (&errloc, 0, "%s", errmsg);
		  grecs_error (NULL, 0, _("original json was: %s"),
			       conn->resp.content);
		  grecs_error (NULL, 0, _("original status line: %s %s %s"),
			       conn->status_line[0],
			       conn->status_line[1],
			       conn->status_line[2]);
		  exit (EX_UNAVAILABLE);
		}
	    }
	}
    }
}

static void
fp_writer (void *closure, char const *text, size_t len)
{
  fwrite (text, len, 1, (FILE*) closure);
}

static void
print_json (FILE *fp, struct json_value *v)
{
  struct json_format fmt = {
    .indent = 2,
    .precision = 0,
    .write = fp_writer,
    .data = fp
  };
  json_value_format (v, &fmt, 0);
  fputc ('\n', fp);
}

static void
shttp_format_result (struct shttp_connection *conn, FILE *fp)
{
  fprintf (stderr, _("%s: raw JSON reply follows:\n"), program_name);
  print_json (fp, conn->result);
}

static void
shttp_print_error (struct shttp_connection *conn)
{
  struct json_value *jv;

  if (conn->result && (jv = json_value_lookup (conn->result, "message")))
    {
      if (jv->type == json_string)
	grecs_error (NULL, 0, "%s", jv->s);
      else
	shttp_format_result (conn, stderr);
    }
  else
    grecs_error (NULL, 0, "%s", conn->status_line[2]);
}

static void
shttp_fatal (struct shttp_connection *conn)
{
  int status;

  shttp_print_error (conn);

  switch (conn->resp.code)
    {
    case 400:
    case 405:
    case 406:
    case 501:
    case 505:
      grecs_error (NULL, 0, _("please report"));
      status = EX_SOFTWARE;
      break;

    case 401:
      status = EX_NOUSER;
      break;

    case 403:
      status = EX_NOPERM;
      break;

    case 404:
      status = EX_NOINPUT;
      break;

    default:
      if (conn->resp.code >= 400 && conn->resp.code < 500)
	status = EX_TEMPFAIL;
      else
	status = EX_UNAVAILABLE;
    }

  exit (status);
}

static void
echo_off (struct termios *stored_settings)
{
  struct termios new_settings;
  tcgetattr (0, stored_settings);
  new_settings = *stored_settings;
  new_settings.c_lflag &= (~ECHO);
  tcsetattr (0, TCSANOW, &new_settings);
}

static void
echo_on (struct termios *stored_settings)
{
  tcsetattr (0, TCSANOW, stored_settings);
}

static char *
getans (int echo, const char *fmt, ...)
{
  struct termios stored_settings;
  char *reply = NULL;
  size_t reply_len = 0;
  size_t len;
  va_list ap;

  va_start (ap, fmt);
  vprintf (fmt, ap);
  va_end (ap);
  printf (": ");
  fflush (stdout);
  if (!echo)
    echo_off (&stored_settings);
  grecs_getline (&reply, &reply_len, stdin);
  if (!echo)
    {
      echo_on (&stored_settings);
      putchar ('\n');
      fflush (stdout);
    }
  len = strlen (reply);
  if (len)
    reply[len - 1] = 0;
  return reply;
}

static void
shttp_get_credentials (struct shttp_connection *conn)
{
  char const *val = shttp_io_header_get (&conn->resp, "WWW-Authenticate");
  char *realm = NULL;

  if (val)
    {
      struct wordsplit ws;
      if (wordsplit (val, &ws,
		     WRDSF_NOVAR | WRDSF_NOCMD
		     | WRDSF_QUOTE | WRDSF_SQUEEZE_DELIMS))
	grecs_error (NULL, 0, "wordsplit: %s", wordsplit_strerror (&ws));
      else
	{
	  if (ws.ws_wordc > 0)
	    {
	      size_t i;

	      if (strcmp (ws.ws_wordv[0], "Basic"))
		{
		  grecs_error (NULL, 0,
			       _("unsupported authentication type: %s"),
			       ws.ws_wordv[0]);
		  shttp_fatal (conn);
		}
	      for (i = 1; i < ws.ws_wordc; i++)
		if (strncmp (ws.ws_wordv[i], "realm=", 6) == 0)
		  {
		    realm = grecs_strdup (ws.ws_wordv[i] + 6);
		    break;
		  }
	    }
	  wordsplit_free (&ws);
	}
    }

  if (!realm)
    realm = grecs_strdup ("Server");

  conn->url->user = getans (1, _("%s user"), realm);
  conn->url->passwd = getans (0, _("%s password"), realm);

  grecs_free (realm);
}

static void
shttp_process (struct shttp_connection *conn, int method, char const *uri)
{
  for (;;)
    {
      shttp_request_send (conn, method, uri);
      shttp_get_reply (conn);
      if (dump && conn->result)
	shttp_format_result (conn, stdout);

      if (conn->resp.code / 100 == 2)
	return;

      switch (conn->resp.code)
	{
	case 401:
	  if (isatty (fileno (stdin)))
	    shttp_get_credentials (conn);
	  else
	    shttp_fatal (conn);
	  break;

	case 404: /* Not found */
	case 409: /* Conflict */
	  exit_status = EX_PIES_NOTFOUND;
	  shttp_print_error (conn);
	  return;

	default:
	  shttp_fatal (conn);
	}
    }
}

static struct json_value *
shttp_getval (struct shttp_connection *conn, char const *name,
	      enum json_value_type type)
{
  struct json_value *p = NULL;
  if (json_object_get (conn->result, name, &p))
    {
      if (errno == ENOENT)
	grecs_error (NULL, 0, _("no \"%s\" member"), name);
      else
	grecs_error (NULL, errno, _("can't get value of \"%s\""), name);
    }
  else if (p->type != type)
    {
      grecs_error (NULL, 0, _("\"%s\" has wrong type"), name);
      p = NULL;
    }
  return p;
}

struct kwtrans
{
  char const *name;
  int c;
};

struct kwtrans mode_trans[] = {
  { "exec",         'C' },
  { "accept",       'A' },
  { "inetd",        'I' },
  { "pass_fd",      'P' },
  { "startup",      'S' },
  { "shutdown",     'E' },
  { "wait",         'W' },
  { "once",         'c' },
  { "boot",         'B' },
  { "bootwait",     'w' },
  { "powerfail",    'F' },
  { "powerwait",    'f' },
  { "powerokwait",  'o' },
  { "ctrlaltdel",   '3' },
  { "ondemand",     'D' },
  { "sysinit",      'i' },
  { "powerfailnow", 'n' },
  { "kbrequest",    'k' },
  { NULL }
};

struct kwtrans status_trans[] = {
  { "running",  'R' },
  { "stopped",  'T' },
  { "listener", 'L' },
  { "sleeping", 's' },
  { "stopping", 'S' },
  { "finished", 'f' },
  { NULL }
};

static int
kwtoc (char const *s, struct kwtrans const *trans)
{
  for (; trans->name; trans++)
    {
      if (strcmp (trans->name, s) == 0)
	return trans->c;
    }
  return '-';
}

static struct json_value *
getval (struct json_value *v, char const *name, enum json_value_type type,
	int optional)
{
  struct json_value *p = NULL;
  if (json_object_get (v, name, &p))
    {
      if (errno == ENOENT)
	{
	  if (!optional)
	    grecs_error (NULL, 0, _("no \"%s\" member"), name);
	}
      else
	grecs_error (NULL, errno, _("can't get value of \"%s\""), name);
    }
  else if (p->type != type)
    {
      grecs_error (NULL, 0, _("\"%s\" has wrong type"), name);
      p = NULL;
    }

  return p;
}

static void
print_comp (FILE *fp, struct json_value *v, size_t n)
{
  struct json_value *p;
  char const *type;
  char fbuf[5];
  int fidx = 0;
  int status = -1;

  if (v->type != json_object)
    {
      grecs_error (NULL, 0, _("%lu: unexpected value type"),
		   (unsigned long) n);
      print_json (fp, v);
      return;
    }

  p = getval (v, "type", json_string, 0);
  if (!p)
    {
      print_json (fp, v);
      return;
    }
  type = p->s;

  p = getval (v, "tag", json_string, 0);
  if (!p)
    {
      print_json (fp, v);
      return;
    }

  fprintf (fp, "%-16s ", p->s);

  if (strcmp (type, "component") == 0)
    {
      p = getval (v, "mode", json_string, 0);
      if (p)
	fbuf[fidx++] = kwtoc (p->s, mode_trans);
      else
	fbuf[fidx++] = '-';
      p = getval (v, "active", json_bool, 0);
      if (p && p->b)
	{
	  p = getval (v, "status", json_string, 0);
	  if (p)
	    fbuf[fidx++] = status = kwtoc (p->s, status_trans);
	  else
	    fbuf[fidx++] = '-';
	}
      else
	fbuf[fidx++] = '-';
    }
  else if (strcmp (type, "command") == 0)
    {
      fbuf[fidx++] = 'E';
    }
  else
    {
      fbuf[fidx++] = '-';
    }

  fbuf[fidx++] = 0;
  fprintf (fp, "%-8.8s ", fbuf);

  if (strcmp (type, "component") == 0)
    {
      p = getval (v, "PID", json_number, 1);
      if (p)
	fprintf (fp, "%10.0f ", p->n);
      else if (status == 'L' /* listener */
	       && (p = getval (v, "URL", json_string, 1)))
	fprintf (fp, "%-10s ", p->s);
      else
	fprintf (fp, "%-10s ", "N/A");

      if (status == 's') /* sleeping */
	{
	  p = getval (v, "wakeup-time", json_number, 0);
	  if (p)
	    {
	      time_t t = (time_t) p->n;
	      fprintftime (fp, "%H:%M:%S", localtime (&t), 0, 0);
	    }
	}

      p = getval (v, "argv", json_array, 0);
      if (p)
	{
	  size_t i, n = json_array_length (p);

	  for (i = 0; i < n; i++)
	    {
	      struct json_value *elt;
	      if (json_array_get (p, i, &elt) == 0)
		fprintf (fp, " %s", elt->s);
	    }
	}
    }
  else if (strcmp (type, "command") == 0)
    {
      p = getval (v, "command", json_string, 0);
      if (p)
	fprintf (fp, "%s", p->s);
    }
  fputc ('\n', fp);
}

struct cmdline_parser_state
{
  char const *command;
  int argc;
  char **argv;
  struct shttp_connection *conn;
};

static char const *
next_token (struct cmdline_parser_state *state)
{
  if (state->argc == 0)
    return NULL;
  --state->argc;
  return *state->argv++;
}

static char const *
peek_token (struct cmdline_parser_state *state)
{
  if (state->argc == 0)
    return NULL;
  return *state->argv;
}

static void
parse_error (char const *fmt, ...)
{
  va_list ap;
  va_start (ap, fmt);
  fflush (stdout);
  fprintf (stderr, "%s: ", program_name);
  vfprintf (stderr, fmt, ap);
  fputc ('\n', stderr);
  va_end (ap);
  exit (EX_USAGE);
}

static char const *
require_token (struct cmdline_parser_state *state)
{
  char const *tok = next_token (state);
  if (!tok)
    parse_error ("%s", _("unexpected end of statement"));
  return tok;
}

static void
assert_eol (struct cmdline_parser_state *state)
{
  char const *tok = next_token (state);
  if (tok)
    parse_error (_("expected end of statement, but found \"%s\""), tok);
}

static void
acc_writer (void *closure, char const *text, size_t len)
{
  grecs_txtacc_grow ((struct grecs_txtacc *)closure, text, len);
}

static char *
piesctl_format (struct json_value *val, char const *base)
{
  char *ret = NULL;
  struct grecs_txtacc *acc;

  acc = grecs_txtacc_create ();
  if (base)
    {
      grecs_txtacc_grow_string (acc, base);
      if (val)
	grecs_txtacc_grow_char (acc, '?');
    }
  if (val)
    {
      struct json_format fmt = {
	.indent = 0,
	.precision = 0,
	.write = acc_writer,
	.data = acc
      };
      json_value_format (val, &fmt, 0);
    }
  grecs_txtacc_grow_char (acc, 0);

  ret = grecs_txtacc_finish (acc, 1);
  grecs_txtacc_free (acc);
  return ret;
}

static struct json_value *
json_encode_op (char const *op, struct json_value *arg)
{
  struct json_value *ret = json_new_object ();
  json_object_set (ret, "op", json_new_string (op));
  if (arg)
    json_object_set (ret, "arg", arg);
  return ret;
}

static void pcond_parse_or (struct cmdline_parser_state *, struct json_value **);

static void
pcond_parse_unary (struct cmdline_parser_state *state, struct json_value **ret)
{
  static char *arg_kw[] = {
    "type", "mode", "status", "component", NULL
  };
  char const *term = require_token (state);

  if (strcasecmp (term, "all") == 0)
    *ret = json_new_bool (1);
  else if (strcasecmp (term, "active") == 0)
    *ret = json_encode_op (term, NULL);
  else if (is_array_member (arg_kw, term))
    {
      if (!peek_token (state))
	parse_error (_("%s requires argument"), term);
      *ret = json_encode_op (term, json_new_string (next_token (state)));
    }
  else if (strcasecmp (term, "not") == 0)
    {
      struct json_value *val;
      if (!peek_token (state))
	parse_error (_("%s requires argument"), term);
      pcond_parse_unary (state, &val);
      *ret = json_encode_op (term, val);
    }
  else if (term[0] == '(')
    {
      pcond_parse_or (state, ret);
      term = next_token (state);
      if (!term || term[0] != ')')
	parse_error ("%s", _("unbalanced parenthesis"));
    }
  else
    parse_error (_("parse error at %s"), term);
}

static int
is_op (struct json_value *val, char const *opcode)
{
  struct json_value *op;

  if (json_object_get (val, "op", &op) == 0)
    {
      if (op->type == json_string && strcmp (op->s, opcode) == 0)
	return 1;
    }
  return 0;
}

static void
binop_append_optimized (struct json_value *ar, struct json_value *val,
			char const *opcode)
{
  if (is_op (val, opcode))
    {
      size_t i, n;
      struct json_value *arg;

      json_object_get (val, "arg", &arg);
      n = json_array_length (arg);
      for (i = 0; i < n; i++)
	{
	  struct json_value *elt;
	  json_array_get (arg, i, &elt);
	  json_array_set (arg, i, NULL);
	  json_array_append (ar, elt);
	}
      json_value_free (val);
    }
  else
    json_array_append (ar, val);
}

static void
pcond_parse_and (struct cmdline_parser_state *state, struct json_value **ret)
{
  char const *token;
  struct json_value *left, *right, *ar;

  pcond_parse_unary (state, &left);
  token = peek_token (state);
  if (!token || strcmp (token, "and"))
    {
      *ret = left;
      return;
    }
  next_token (state);
  pcond_parse_and (state, &right);
  ar = json_new_array ();
  json_array_append (ar, left);
  binop_append_optimized (ar, right, token);
  *ret = json_encode_op (token, ar);
}

static void
pcond_parse_or (struct cmdline_parser_state *state, struct json_value **ret)
{
  char const *token;
  struct json_value *left, *right, *ar;

  pcond_parse_and (state, &left);
  token = peek_token (state);
  if (!token || strcmp (token, "or"))
    {
      *ret = left;
      return;
    }
  next_token (state);
  pcond_parse_or (state, &right);
  ar = json_new_array ();
  json_array_append (ar, left);
  binop_append_optimized (ar, right, token);
  *ret = json_encode_op (token, ar);
}

static struct json_value *
parse_condition (struct cmdline_parser_state *state)
{
  struct json_value *val = NULL;

  if (state->argc > 0)
    {
      pcond_parse_or (state, &val);
      assert_eol (state);
#if 0
      print_json (stdout, val);
      exit (0);
#endif
    }
  return val;
}

static char *
parse_condition_to_uri (char const *base, struct cmdline_parser_state *state,
			int mandatory)
{
  struct json_value *val;
  char *ret;
  if (mandatory && state->argc == 0)
    parse_error ("%s", _("condition must be specified"));
  val = parse_condition (state);
  ret = piesctl_format (val, base);
  json_value_free (val);
  return ret;
}

/* Provide default condition if there are no more arguments */
static struct cmdline_parser_state *
default_cond (struct cmdline_parser_state *state)
{
  if (!peek_token (state))
    {
      static char *dfl[] = { "type", "component", NULL };
      state->argc = sizeof (dfl) / sizeof (dfl[0]);
      state->argv = dfl;
    }
  return state;
}

static void
com_list (struct cmdline_parser_state *state)
{
  struct shttp_connection *conn = state->conn;
  char *uri = parse_condition_to_uri ("/programs", default_cond (state), 0);

  shttp_io_init (&conn->req);
  shttp_process (conn, METH_GET, uri);
  grecs_free (uri);
  if (conn->resp.code == 200 && !dump
      && conn->result && conn->result->type == json_array)
    {
      size_t i, n = json_array_length (conn->result);

      for (i = 0; i < n; i++)
	{
	  struct json_value *v;

	  if (json_array_get (conn->result, i, &v) == 0)
	    print_comp (stdout, v, i);
	}
    }
}

static int
json_object_get_type (struct json_value *obj, char const *name,
		      enum json_value_type type, struct json_value **ret)
{
  struct json_value *v;
  int rc = json_object_get (obj, name, &v);
  if (rc)
    return rc;
  if (v->type != type)
    {
      errno = EACCES;
      return -1;
    }
  *ret = v;
  return 0;
}

static struct json_value *
json_object_require_type (struct json_value *obj, char const *name,
			  enum json_value_type type)
{
  struct json_value *val;

  if (json_object_get_type (obj, name, type, &val))
    {
      grecs_error (NULL, 0, _("can't get attribute %s"), name);
      //      shttp_format_result (conn, stderr);
      exit (EX_PROTOCOL);
    }
  return val;
}

static void
shttp_print_response_status (struct shttp_connection *conn)
{
  if (!dump && conn->result && conn->result->type == json_array)
    {
      size_t i, n = json_array_length (conn->result);

      if (n == 0)
	{
	  grecs_error (NULL, 0, _("no component matches the supplied condition"));
	}
      else
	for (i = 0; i < n; i++)
	  {
	    struct json_value *elt, *v;

	    if (json_array_get (conn->result, i, &elt) == 0)
	      {
		if (elt->type != json_object)
		  {
		    grecs_error (NULL, 0, _("%lu: unexpected value type"),
				 (unsigned long) i);
		    print_json (stderr, v);
		    continue;
		  }
		if (json_object_get_type (elt, "tag", json_string, &v) == 0)
		  {
		    printf ("%s: ", v->s);
		    if (json_object_get_type (elt, "status", json_string, &v) == 0)
		      {
			if (strcmp (v->s, "OK") == 0)
			  fputs (v->s, stdout);
			else if (json_object_get_type (elt, "message",
						       json_string, &v) == 0)
			  fputs (v->s, stdout);
			else
			  printf (_("unknown error"));
		      }
		    else
		      printf (_("unknown status"));
		    fputc ('\n', stdout);
		  }
	      }
	  }
    }
}

static void
com_stop (struct cmdline_parser_state *state)
{
  struct shttp_connection *conn = state->conn;
  char *uri = parse_condition_to_uri ("/programs", default_cond (state), 1);

  shttp_io_init (&conn->req);
  shttp_process (conn, METH_DELETE, uri);
  grecs_free (uri);
  shttp_print_response_status (conn);
}

static void
com_start (struct cmdline_parser_state *state)
{
  struct shttp_connection *conn = state->conn;
  char *uri = parse_condition_to_uri ("/programs", default_cond (state), 1);

  shttp_io_init (&conn->req);
  shttp_process (conn, METH_PUT, uri);
  grecs_free (uri);
  shttp_print_response_status (conn);
}

static void
com_restart (struct cmdline_parser_state *state)
{
  struct shttp_connection *conn = state->conn;

  shttp_io_init (&conn->req);
  conn->req.content = parse_condition_to_uri (NULL, default_cond (state), 1);
  conn->req.content_length = strlen (conn->req.content);
  shttp_process (conn, METH_POST, "/programs");
  shttp_print_response_status (conn);
}

static void
id_fmt_string (struct json_value *val)
{
  fputs (val->s, stdout);
}

static void
id_fmt_pid (struct json_value *val)
{
  printf ("%.0f", val->n);
}

static void
id_fmt_argv (struct json_value *val)
{
  size_t i, n;
  char const *delim = NULL;

  n = json_array_length (val);
  for (i = 0; i < n; i++)
    {
      struct json_value *elt;
      if (json_array_get (val, i, &elt) == 0 && elt->type == json_string)
	{
	  if (delim)
	    fputs (delim, stdout);
	  fputs (elt->s, stdout);
	  delim = " ";
	}
    }
}

struct id_fmt
{
  char *name;
  enum json_value_type type;
  void (*format) (struct json_value *);
};

static struct id_fmt id_fmt[] = {
  { "package",  json_string, id_fmt_string },
  { "version",  json_string, id_fmt_string },
  { "instance", json_string, id_fmt_string },
  { "binary",   json_string, id_fmt_string },
  { "argv",     json_array,  id_fmt_argv },
  { "PID",      json_number, id_fmt_pid },
  { NULL }
};

static struct id_fmt *
find_id_fmt (char const *name)
{
  struct id_fmt *p;

  for (p = id_fmt; p->name; p++)
    if (strcmp (p->name, name) == 0)
      return p;
  return NULL;
}

static void
com_id (struct cmdline_parser_state *state)
{
  struct shttp_connection *conn = state->conn;
  struct json_value *v;
  struct id_fmt *fmt;

  if (state->argc == 0)
    {
      shttp_io_init (&state->conn->req);
      shttp_process (state->conn, METH_GET, "/instance");
      if (state->conn->resp.code == 200 && !dump)
	for (fmt = id_fmt; fmt->name; fmt++)
	  {
	    v = shttp_getval (state->conn, fmt->name, fmt->type);
	    if (v)
	      {
		printf ("%s: ", fmt->name);
		fmt->format  (v);
		putchar ('\n');
	      }
	  }
    }
  else
    {
      char *buf = NULL;
      size_t size = 0;
      size_t i;

      for (i = 0; i < state->argc; i++)
	{
	  grecs_asprintf (&buf, &size, "/instance/%s", state->argv[i]);
	  shttp_io_init (&conn->req);
	  shttp_process (conn, METH_GET, buf);
	  if (conn->resp.code == 200 && !dump)
	    {
	      fmt = find_id_fmt (state->argv[i]);
	      if (fmt)
		{
		  v = shttp_getval (conn, fmt->name, fmt->type);
		  if (v)
		    {
		      printf ("%s: ", fmt->name);
		      fmt->format (v);
		      putchar ('\n');
		    }
		}
	      else if (json_object_get (conn->result, state->argv[i], &v) == 0)
		{
		  printf ("%s: ", state->argv[i]);
		  print_json (stdout, v);
		  putchar ('\n');
		}
	    }
	}
    }
}

static void
com_shutdown (struct cmdline_parser_state *state)
{
  assert_eol (state);
  shttp_io_init (&state->conn->req);
  shttp_process (state->conn, METH_DELETE, "/instance/PID");
}

static void
com_reboot (struct cmdline_parser_state *state)
{
  assert_eol (state);
  shttp_io_init (&state->conn->req);
  shttp_process (state->conn, METH_PUT, "/instance/PID");
}

/*
   <telinit> ::= "telinit" <initcmd>
   <initcmd> ::= <runlevel> | <environ>
   <runlevel> ::= "runlevel" | "runlevel" <level>
   <environ> ::= "environ" "list" [<NAME>] | "environ" "set" <NAME>"="<VALUE>
 */
struct telinit_parser
{
  struct cmdline_parser_state *state;
  int method;
  struct json_value *query;
  char *uri;
  void (*format) (struct shttp_connection *);
};

static struct id_fmt runlevel_fmt[] = {
  { "runlevel", json_string, id_fmt_string },
  { "prevlevel", json_string, id_fmt_string },
  { "bootstate", json_string, id_fmt_string },
  { "initdefault", json_string, id_fmt_string },
  { NULL }
};

static void
telinit_format_runlevel (struct shttp_connection *conn)
{
  struct id_fmt *fmt;

  for (fmt = runlevel_fmt; fmt->name; fmt++)
    {
      struct json_value *v = shttp_getval (conn, fmt->name, fmt->type);
      if (v)
	{
	  printf ("%s: ", fmt->name);
	  fmt->format  (v);
	  putchar ('\n');
	}
    }
}

static void
telinit_parse_runlevel (struct telinit_parser *p)
{
  char const *tok = next_token (p->state);

  p->uri = "/runlevel";
  if (!tok)
    {
      p->method = METH_GET;
      p->format = telinit_format_runlevel;
    }
  else
    {
      p->method = METH_PUT;
      p->query = json_new_object ();
      json_object_set (p->query, "runlevel", json_new_string (tok));
      p->format = NULL;
    }
}

static void
telinit_format_environ (struct shttp_connection *conn)
{
  int err = 0;

  switch (conn->result->type)
    {
    case json_array:
      {
	size_t i, n;

	n = json_array_length (conn->result);
	for (i = 0; i < n; i++)
	  {
	    struct json_value *v;
	    json_array_get (conn->result, i, &v);
	    printf ("%s\n", v->s);
	  }
      }
      break;

    case json_object:
      {
	struct json_value *val;

	val = json_object_require_type (conn->result, "status", json_string);
	if (strcmp (val->s, "OK") == 0)
	  {
	    if (json_object_get_type (conn->result, "value",
				      json_string, &val) == 0)
	      printf ("%s\n", val->s);
	  }
	else if (strcmp (val->s, "ER") == 0)
	  {
	    if (json_object_get_type (conn->result, "message",
				      json_string, &val) == 0)
	      fputs (val->s, stderr);
	    else
	      {
		grecs_error (NULL, 0, "%s", _("unknown error"));
		err = 1;
	      }
	  }
	else
	  {
	    grecs_error (NULL, 0, "%s", _("unrecognized response"));
	    err = 1;
	  }
      }
      break;

    default:
      grecs_error (NULL, 0, "unexpected response type");
      err = 1;
    }
  if (err)
    print_json (stderr, conn->result);
}

static void
telinit_format_modenv (struct shttp_connection *conn)
{
  /* Nothing */
}

static void
telinit_list_environ (struct telinit_parser *p)
{
  char const *tok = next_token (p->state);
  assert_eol (p->state);
  p->method = METH_GET;
  p->format = telinit_format_environ;
  if (tok)
    {
      size_t n = 0;
      grecs_asprintf (&p->uri, &n, "/environ/%s", tok);
    }
  else
    p->uri = "/environ";
}

static void
telinit_mod_environ (struct telinit_parser *p, int set)
{
  char const *tok = next_token (p->state);
  assert_eol (p->state);
  p->method = set ? METH_PUT : METH_DELETE;
  p->format = telinit_format_modenv;
  if (tok)
    {
      size_t n = 0;
      grecs_asprintf (&p->uri, &n, "/environ/%s", tok);
    }
  else
    p->uri = "/environ";
}

static void
telinit_parse_environ (struct telinit_parser *p)
{
  char const *tok = require_token (p->state);

  if (strcmp (tok, "list") == 0)
    telinit_list_environ (p);
  else if (strcmp (tok, "set") == 0)
    telinit_mod_environ (p, 1);
  else if (strcmp (tok, "unset") == 0)
    telinit_mod_environ (p, 0);
  else
    parse_error (_("expected \"list\" or \"set\", but found \"%s\""), tok);
}

static void
telinit_parse (struct telinit_parser *parser)
{
  char const *tok = require_token (parser->state);

  if (strcmp (tok, "runlevel") == 0)
    telinit_parse_runlevel (parser);
  else if (strcmp (tok, "environ") == 0)
    telinit_parse_environ (parser);
  else
    parse_error (_("unrecognized subcommand: %s"), tok);
}

static void
com_telinit (struct cmdline_parser_state *state)
{
  struct shttp_connection *conn = state->conn;
  struct telinit_parser parser;

  parser.state = state;
  parser.method = METH_INVALID;
  parser.query = NULL;
  parser.uri = NULL;
  telinit_parse (&parser);

  shttp_io_init (&conn->req);
  switch (parser.method)
    {
    case METH_GET:
    case METH_DELETE:
      break;

    case METH_PUT:
      {
	struct grecs_txtacc *acc = grecs_txtacc_create ();;
	struct json_format fmt = {
	  .indent = 0,
	  .precision = 0,
	  .write = acc_writer,
	  .data = acc
	};

	json_value_format (parser.query, &fmt, 0);
	grecs_txtacc_grow_char (acc, 0);
	conn->req.content = grecs_txtacc_finish (acc, 1);
	conn->req.content_length = strlen (conn->req.content);
	grecs_txtacc_free (acc);
      }
      break;

    default:
      parse_error ("%s", _("bad syntax"));
    }

  json_value_free (parser.query);

  shttp_process (conn, parser.method, parser.uri);
  if (conn->resp.code / 100 == 2 && !dump && parser.format)
    parser.format (conn);
}

/*
 * piesctl config reload
 * piesctl config file clear
 * piesctl config file add syntax name
 * piesctl config file del[ete] name [name...]
 * piesctl config file list
 */

static void
locus_deserialize (struct grecs_locus *loc, struct json_value *msg)
{
  struct json_value *tmp, *x;

  memset (loc, 0, sizeof *loc);
  if (json_object_get_type (msg, "file", json_array, &tmp) == 0
      && json_array_length (tmp) == 2)
    {
      if (json_array_get (tmp, 0, &x) == 0 && x->type == json_string)
	loc->beg.file = x->s;
      else
	loc->beg.file = "<unknown>";
      if (json_array_get (tmp, 1, &x) == 0 && x->type == json_string)
	loc->end.file = x->s;
      else
	loc->beg.file = "<unknown>";
    }

  if (json_object_get_type (msg, "line", json_array, &tmp) == 0
      && json_array_length (tmp) == 2)
    {
      if (json_array_get (tmp, 0, &x) == 0 && x->type == json_number)
	loc->beg.line = x->n;
      if (json_array_get (tmp, 1, &x) == 0 && x->type == json_number)
	loc->end.line = x->n;
    }

  if (json_object_get_type (msg, "col", json_array, &tmp) == 0
      && json_array_length (tmp) == 2)
    {
      if (json_array_get (tmp, 0, &x) == 0 && x->type == json_number)
	loc->beg.col = x->n;
      if (json_array_get (tmp, 1, &x) == 0 && x->type == json_number)
	loc->end.col = x->n;
    }
}

static void
conf_reload (struct cmdline_parser_state *state)
{
  struct shttp_connection *conn = state->conn;
  struct json_value *val;

  assert_eol (state);
  shttp_io_init (&conn->req);
  shttp_process (conn, METH_PUT, "/conf/runtime");
  if (dump)
    return;
  assert (conn->result && conn->result->type == json_object);

  val = json_object_require_type (conn->result, "status", json_string);
  if (strcmp (val->s, "ER") == 0)
    {
      if (json_object_get_type (conn->result, "message",
				json_string, &val) == 0)
	fputs (val->s, stdout);
      else
	printf ("%s", _("unknown error"));
      fputc ('\n', stdout);
    }

  if (json_object_get_type (conn->result, "parser_messages", json_array,
			    &val) == 0)
    {
      size_t i, n = json_array_length (val);
      struct json_value *msg, *tmp;

      for (i = 0; i < n; i++)
	{
	  struct grecs_locus loc;
	  int err = 1;
	  int ec;

	  if (json_array_get (val, i, &msg) || msg->type != json_object)
	    continue;
	  locus_deserialize (&loc, msg);
	  if (json_object_get_type (msg, "error", json_bool, &tmp) == 0)
	    err = tmp->b;
	  if (json_object_get_type (msg, "syserrno", json_number, &tmp) == 0)
	    ec = tmp->n;

	  tmp = json_object_require_type (msg, "message", json_string);
	  piesctl_diag (&loc, err, ec, tmp->s);
	}
    }
}

static void
conf_file_list (struct cmdline_parser_state *state)
{
  struct shttp_connection *conn = state->conn;
  struct json_value *val, *tmp;
  size_t i, n;

  assert_eol (state);
  shttp_io_init (&conn->req);
  shttp_process (conn, METH_GET, "/conf/files");
  if (dump)
    return;
  assert (conn->result && conn->result->type == json_array);
  n = json_array_length (conn->result);
  for (i = 0; i < n; i++)
    {
      char const *syntax, *file;
      if (json_array_get (conn->result, i, &val) || val->type != json_object)
	continue;
      if (json_object_get_type (val, "syntax", json_string, &tmp))
	continue;
      syntax = tmp->s;
      if (json_object_get_type (val, "file", json_string, &tmp))
	continue;
      file = tmp->s;
      printf ("%-16s%s\n", syntax, file);
    }
}

static void
conf_file_clear (struct cmdline_parser_state *state)
{
  struct shttp_connection *conn = state->conn;
  struct json_value *val;

  assert_eol (state);
  shttp_io_init (&conn->req);
  shttp_process (conn, METH_DELETE, "/conf/files");
  val = json_object_require_type (conn->result, "status", json_string);
  if (strcmp (val->s, "OK"))
    {
      if (json_object_get_type (conn->result, "message",
				json_string, &val) == 0)
	fputs (val->s, stderr);
      else
	fputs (_("unknown error"), stderr);
      fputc ('\n', stderr);
    }
}

static void
conf_file_add (struct cmdline_parser_state *state)
{
  struct shttp_connection *conn = state->conn;
  struct json_value *val;
  char const *name, *syntax;

  syntax = require_token (state);
  name = require_token (state);

  val = json_new_object ();
  json_object_set (val, "syntax", json_new_string (syntax));
  json_object_set (val, "file", json_new_string (name));

  shttp_io_init (&conn->req);
  conn->req.content = piesctl_format (val, NULL);
  conn->req.content_length = strlen (conn->req.content);
  shttp_process (conn, METH_POST, "/conf/files");
  // FIXME conn->resp.code == 201?
}

static void
conf_file_del (struct cmdline_parser_state *state)
{
  struct shttp_connection *conn = state->conn;
  struct json_value *val = json_new_array ();
  char const *tok;
  char *uri;
  struct cmdline_parser_state save_state = *state;

  while ((tok = next_token (state)))
    json_array_append (val, json_new_string (tok));

  shttp_io_init (&conn->req);
  uri = piesctl_format (val, "/conf/files");
  json_value_free (val);

  shttp_process (conn, METH_DELETE, uri);
  free (uri);

  val = json_object_require_type (conn->result, "status", json_string);
  if (strcmp (val->s, "OK") == 0)
    {
      if (json_object_get_type (conn->result, "message",
				json_string, &val) == 0)
	{
	  fputs (val->s, stderr);
	  fputc ('\n', stderr);
	}
      if (json_object_get_type (conn->result, "result", json_array, &val) == 0)
	{
	  size_t i, n = json_array_length (val);
	  assert (n == save_state.argc);
	  for (i = 0; i < n; i++)
	    {
	      struct json_value *v;
	      if (json_array_get (val, i, &v) == 0
		  && v->type == json_bool
		  && !v->b)
		{
		  fputs (save_state.argv[i], stderr);
		  fputc ('\n', stderr);
		}
	    }
	}
    }
  else
    {
      if (json_object_get_type (conn->result, "message",
				json_string, &val) == 0)
	fputs (val->s, stderr);
      else
	fputs (_("unknown error"), stderr);
      fputc ('\n', stderr);
    }
}

static void
conf_file (struct cmdline_parser_state *state)
{
  char const *tok = require_token (state);
  if (strcmp (tok, "add") == 0)
    conf_file_add (state);
  else if (strcmp (tok, "del") == 0 || strcmp (tok, "delete") == 0)
    conf_file_del (state);
  else if (strcmp (tok, "list") == 0)
    conf_file_list (state);
  else if (strcmp (tok, "clear") == 0)
    conf_file_clear (state);
  else
    parse_error (_("unexpected word: %s"), tok);
}

static void
conf_parse (struct cmdline_parser_state *state)
{
  char const *tok = require_token (state);

  if (strcmp (tok, "reload") == 0)
    conf_reload (state);
  else if (strcmp (tok, "file") == 0)
    conf_file (state);
  else
    parse_error (_("unexpected word: %s"), tok);
}

static void
com_config (struct cmdline_parser_state *state)
{
  conf_parse (state);
}

typedef void (*ctlcom_t) (struct cmdline_parser_state *);

struct comtab
{
  char const *name;
  char const *argdoc;
  char const *docstr;
  ctlcom_t command;
};

static struct comtab comtab[] = {
  { "list",    N_("[CONDITION]"),
    N_("list configured components"), com_list },
  { "stop",    N_("CONDITION"),
    N_("stop components"),      com_stop },
  { "start",   N_("CONDITION"),
    N_("start components"),     com_start },
  { "restart", N_("CONDITION"),
    N_("restart components"),   com_restart },
  { NULL, NULL,
    /* TRANSLATORS: You can leave the BNF below untranslated.  If you
       chose to translate it, please do not translate words in double
       quotes. */
    N_("CONDITION is defined as follows:\n\
\n\
<condition> ::= <disjunction>\n\
<disjunction> ::= <conjunction> | <conjunction> \"or\" <disjunction>\n\
<conjunction> ::= <unary> | <unary> \"and\" <conjunction>\n\
<unary> ::= <term> | \"not\" <condition> | \"(\" <condition> \")\"\n\
<term> ::= \"all\" | \"active\" | <keyword> <value>\n\
<keyword> ::= \"type\" | \"mode\" | \"status\" | \"component\"\n\
<value> ::= <word> | <quoted-string>\n\
<word> ::= <printable> | <word> <printable>\n\
<printable> ::= \"A\" - \"Z\" | \"a\" - \"z\" | \"0\" - \"9\" |\
\"_\" | \".\" | \"*\" | \":\" | \"@\" | \"[\" | \"]\" | \"-\" | \"/\"\n\
<quoted-string> ::= \"\"\" <string> \"\"\"\n\
<string> ::= <char> | <string> <char>\n\
<char> ::= <any character except \"\\\" and \"\"\"> | \"\\\\\" | \"\\\"\""),
    NULL },

  { "id",      N_("[KEYWORDS...]"),
    N_("show info about the running GNU Pies instance"),
    com_id },
  { NULL, NULL,
    /* TRANSLATORS: Please don't translate the keywords. */
    N_("Available KEYWORDS are: package, version, instance, binary, argv, PID"), NULL },
  { "shutdown", "",
    N_("stop running pies instance"), com_shutdown },
  { "reboot", "",
    N_("restart pies instance"), com_reboot },

  { "config",  "reload",
    N_("reload configuration"), com_config },
  { NULL,      "file clear",
    N_("clear configuration file list"), NULL },
  /* TRANSLATORS: Translate only words in upper case */
  { NULL,     N_("file add SYNTAX FILE"),
    N_("add FILE of given SYNTAX to the list of configuration files"), NULL },
  /* TRANSLATORS: Translate only words in upper case */
  { NULL,     N_("file del[ete] NAME [NAME...]"),
    N_("remove listed names from the list of configuration files"), NULL },
  { NULL,      "file list",
    N_("list configuration files"), NULL },

  { "telinit", "runlevel [N]", N_("list or change the runlevel"),
    com_telinit },
  /* TRANSLATORS: Translate only words in upper case */
  { NULL,     N_("environ list [NAME]"),
    N_("list init execution environment"), NULL },
  /* TRANSLATORS: Translate only words in upper case */
  { NULL,     N_("environ set NAME=VALUE"),
    N_("update execution environment"), NULL },
  /* TRANSLATORS: Translate only words in upper case */
  { NULL,     N_("environ unset NAME"),
    N_("update execution environment"), NULL },
  { NULL }
};

static int
comtabend (struct comtab *p)
{
  return !p->name && !p->argdoc && !p->docstr;
}

static struct comtab *
find_command (char const *name)
{
  struct comtab *p, *match = NULL;
  size_t match_count = 0;
  size_t len = strlen (name);

  for (p = comtab; !comtabend (p); p++)
    {
      if (!p->name)
	continue;

      if (len <= strlen (p->name)
	  && memcmp (p->name, name, len) == 0)
	{
	  if (!match)
	    {
	      match = p;
	      ++match_count;
	    }
	  else
	    {
	      if (match_count == 1)
		fprintf (stderr, _("%s: ambiguous command %s:\n"),
			 program_name, name);
	      ++match_count;
	      fprintf (stderr, "    %s\n", p->name);
	    }
	}
    }

  if (match_count == 0)
    parse_error (_("unknown command: %s"), name);
  else if (match_count > 1)
    exit (EX_USAGE);

  return match;
}

static void
command_help_hook (WORDWRAP_FILE wf, struct parseopt *po)
{
  struct comtab *cp;
  char const *command = NULL;

  wordwrap_printf (wf, "%s\n",
		   _("Available commands with their arguments are:"));

  for (cp = comtab; !comtabend (cp); cp++)
    {
      if (cp->argdoc)
	{
	  if (cp->name)
	    {
	      wordwrap_putc (wf, '\n');
	      command = cp->name;
	    }

	  if (command)
	    {
	      wordwrap_set_left_margin (wf,
					parseopt_help_format.short_opt_col);
	      wordwrap_puts (wf, command);
	      wordwrap_putc (wf, ' ');
	    }
	  else
	    wordwrap_set_left_margin (wf,
				      parseopt_help_format.long_opt_col);
	  if (cp->argdoc[0])
	    wordwrap_puts (wf, gettext (cp->argdoc));

	  wordwrap_set_left_margin (wf,
				    parseopt_help_format.long_opt_col);
	}
      else
	{
	  wordwrap_putc (wf, '\n');
	  wordwrap_set_left_margin (wf,
				    parseopt_help_format.long_opt_col);
	}

      if (cp->docstr[0])
	{
	  wordwrap_puts (wf, gettext (cp->docstr));
	}
      wordwrap_putc (wf, '\n');
    }
}

char *incpath = DEFAULT_INCLUDE_PATH;

enum
{
  OPT_CONFIG_HELP = 256,
  OPT_PREPROCESSOR,
  OPT_NO_PREPROCESSOR,
  OPT_NO_INCLUDE,
};

static int
optset_config_help (struct parseopt *po, struct optdef *opt, char *arg)
{
  config_help ();
  exit (0);
}

static int
optset_preprocessor (struct parseopt *po, struct optdef *opt, char *arg)
{
  if (DEFAULT_PREPROCESSOR)
    * (char**) opt->opt_ptr = arg;
  return 0;
}

static int
optset_url (struct parseopt *po, struct optdef *opt, char *arg)
{
  if (pies_url_create (&client.url, arg))
    {
      grecs_error (NULL, 0, _("%s: cannot create URL: %s"),
		   arg, strerror (errno));
      exit (EX_USAGE);
    }
  return 0;
}

static int
optset_pp_option (struct parseopt *po, struct optdef *opt, char *arg)
{
  char optbuf[3] = { '-', 0, 0 };
  while (opt->opt_name[1])
    {
      ++opt;
      if (!(opt->opt_flags & OPTFLAG_ALIAS))
	{
	  po->po_error (po, PO_MSG_ERR,
			"INTERNAL ERROR at %s:%d, please report",
			__FILE__, __LINE__);
	  exit (EX_SOFTWARE);
	}
    }
  optbuf[1] = opt->opt_name[0];
  pp_add_option (optbuf, arg);
  return 0;
}

static struct optdef options[] = {
  {
    .opt_name = "instance",
    .opt_argdoc = N_("NAME"),
    .opt_doc = N_("connect to instance NAME"),
    .opt_set = optset_string_copy,
    .opt_ptr = &instance
  },
  {
    .opt_name = "i",
    .opt_flags = OPTFLAG_ALIAS
  },

  {
    .opt_name = "config-file",
    .opt_argdoc = N_("FILE"),
    .opt_doc = N_("use FILE instead of the default configuration"),
    .opt_set = optset_string_copy,
    .opt_ptr = &config_file
  },
  {
    .opt_name = "c",
    .opt_flags = OPTFLAG_ALIAS
  },

  {
    .opt_name = "config-help",
    .opt_doc = N_("show configuration file summary"),
    .opt_code = OPT_CONFIG_HELP,
    .opt_set = optset_config_help
  },

  {
    .opt_name = "E",
    .opt_doc = N_("preprocess configuration files and exit"),
    .opt_set = optset_true,
    .opt_ptr = &preprocess_only
  },

  {
    .opt_name = "verbose",
    .opt_doc = N_("verbose diagnostics"),
    .opt_set = optset_incr,
    .opt_ptr = &verbose
  },
  {
    .opt_name = "v",
    .opt_flags = OPTFLAG_ALIAS
  },

  {
    .opt_name = "dump",
    .opt_doc = N_("dump obtained responses verbatim"),
    .opt_set = optset_true,
    .opt_ptr = &dump
  },
  {
    .opt_name = "d",
    .opt_flags = OPTFLAG_ALIAS
  },

  {
    .opt_name = "url",
    .opt_argdoc = N_("URL"),
    .opt_doc = N_("connect to this socket"),
    .opt_set = optset_url
  },
  {
    .opt_name = "u",
    .opt_flags = OPTFLAG_ALIAS
  },

  {
    .opt_name = "no-netrc",
    .opt_doc = N_("don't read ~/.netrc file"),
    .opt_set = optset_true,
    .opt_ptr = &no_netrc_option
  },
  {
    .opt_name = "N",
    .opt_flags = OPTFLAG_ALIAS
  },

  {
    .opt_flags = OPTFLAG_DOC,
    .opt_doc = N_("Preprocessor")
  },

  {
    .opt_name = "preprocessor",
    .opt_argdoc = N_("COMMAND"),
    .opt_doc = N_("use COMMAND instead of the default preprocessor"),
    .opt_code = OPT_PREPROCESSOR,
    .opt_set = optset_preprocessor,
    .opt_ptr = &grecs_preprocessor
  },

  {
    .opt_name = "no-preprocessor",
    .opt_doc = N_("disable preprocessing"),
    .opt_code = OPT_NO_PREPROCESSOR,
    .opt_set = optset_string_copy,
    .opt_ptr = &grecs_preprocessor
  },

  {
    .opt_name = "include-directory",
    .opt_argdoc = N_("DIR"),
    .opt_doc = N_("add include directory"),
    .opt_set = optset_pp_option
  },
  {
    .opt_name = "I",
    .opt_flags = OPTFLAG_ALIAS
  },

  {
    .opt_name = "no-include",
    .opt_doc = N_("clear default preprocessor search path"),
    .opt_code = OPT_NO_INCLUDE,
    .opt_set = optset_string_copy,
    .opt_ptr = &incpath
  },
  {
    .opt_name = "define",
    .opt_argdoc = N_("NAME[=VALUE]"),
    .opt_doc =
    N_("define a preprocessor symbol NAME as having VALUE or empty"),
    .opt_set = optset_pp_option
  },
  {
    .opt_name = "D",
    .opt_flags = OPTFLAG_ALIAS
  },
  {
    .opt_name = "undefine",
    .opt_argdoc = N_("NAME"),
    .opt_doc = N_("undefine a preprocessor symbol NAME"),
    .opt_set = optset_pp_option
  },
  {
    .opt_name = "U",
    .opt_flags = OPTFLAG_ALIAS
  },

  { NULL }
}, *optdef[] = { options, NULL };

extern void version_hook (WORDWRAP_FILE wf, struct parseopt *po);

static struct parseopt po = {
  .po_descr = N_("GNU pies control program"),
  .po_argdoc = N_("COMMAND [ARG...]"),
  .po_optdef = optdef,
  .po_package_name = PACKAGE_NAME,
  .po_package_url = PACKAGE_URL,
  .po_bugreport_address = PACKAGE_BUGREPORT,
  .po_help_hook = command_help_hook,
  .po_version_hook = version_hook
};

int
main (int argc, char **argv)
{
  struct comtab *cmd;
  struct cmdline_parser_state state;

  set_program_name (argv[0]);
#ifdef ENABLE_NLS
  setlocale (LC_ALL, "");
  bindtextdomain (PACKAGE, LOCALEDIR);
  textdomain (PACKAGE);
#endif
  grecs_print_diag_fun = piesctl_diag;
  grecs_preprocessor = DEFAULT_PREPROCESSOR;

  if (parseopt_getopt (&po, argc, argv) == OPT_ERR)
    {
      grecs_error (NULL, errno, _("parseopt_getopt failed"));
      exit (EX_SOFTWARE);
    }
  parseopt_argv (&po, &argc, &argv);
  parseopt_free (&po);

  if (argc == 0)
    parse_error ("%s", _("not enough arguments"));

  pp_init (incpath);
  setenv ("PIES_INSTANCE", instance, 1);

  parse_config ();

  cmd = find_command (argv[0]);
  state.command = cmd->name;
  state.argc = argc - 1;
  state.argv = argv + 1;
  state.conn = shttp_connect (client.url, client.source_addr);
  if (!state.conn)
    return EX_UNAVAILABLE;
  cmd->command (&state);
  shttp_close (state.conn);
  return exit_status;
}
