/* This file is part of GNU Pies.
   Copyright (C) 2015-2019 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 "fprintftime.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 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);

#include "piesctl-cl.h"

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_locus_t *locus,
		   void *varptr,
		   grecs_value_t *value, void *cb_data)
{
  switch (cmd)
    {
    case grecs_callback_section_begin:
      if (GRECS_VALUE_EMPTY_P (value))
	{
	  grecs_error (locus, 0, _("missing tag"));
	  return 1;
	}
      if (assert_grecs_value_type (&value->locus, value, GRECS_TYPE_STRING))
	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_include_path_setup (DEFAULT_VERSION_INCLUDE_DIR,
			    DEFAULT_INCLUDE_DIR, NULL);
  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_preproc_run (file_name, grecs_preprocessor)
	      ? 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)
    {
      /* 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_preproc_run (file_name, grecs_preprocessor)
		  ? EX_CONFIG : EX_OK);
	  tree = grecs_parse (file_name);
	  node = grecs_find_node (tree, "/control/socket");
	  if (node)
	    {
	      assert_grecs_value_type (&node->locus,
				       node->v.value, GRECS_TYPE_STRING);
	      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;
  union pies_sockaddr_storage addr;
  socklen_t socklen;
  int flags;
  struct shttp_connection *conn;
  FILE *fp;
  
  if (strcmp (url->scheme, "unix") == 0
      || strcmp (url->scheme, "file") == 0
      || strcmp (url->scheme, "socket") == 0)
    {
      struct stat st;
		
      if (url->port)
	{
	  grecs_error (NULL, 0, _("%s: invalid connection type: "
				  "port is meaningless for UNIX sockets"),
		       url->string);
	  return NULL;
	}
		
      if (strlen (url->path) > sizeof addr.s_un.sun_path)
	{
	  errno = EINVAL;
	  grecs_error (NULL, 0,
		       _("%s: UNIX socket name too long"),
		       url->path);
	  return NULL;
	}
		
      addr.s.sa_family = PF_UNIX;
      socklen = sizeof (addr.s_un);
      strcpy (addr.s_un.sun_path, url->path);
		
      if (stat (url->path, &st))
	{
	  if (errno != ENOENT)
	    {
	      grecs_error (NULL, errno, _("%s: %s failed"), url->path, "stat");
	      return NULL;
	    }
	}
      else
	{
	  if (!S_ISSOCK (st.st_mode))
	    {
	      grecs_error (NULL, 0, _("%s: not a socket"), url->path);
	      return NULL;
	    }
	}
    }
  else if (strcmp (url->scheme, "inet") == 0)
    {
      short pnum;
      struct hostent *hp;
      
      addr.s_in.sin_family = PF_INET;
      socklen = sizeof (addr.s_in);

      pnum = url->port ? url->port : 8080;
      
      hp = gethostbyname (url->host);
      if (!hp)
	{
	  grecs_error (NULL, 0, _("%s: unknown host name"), url->string);
	  return NULL;
	}

      addr.s_in.sin_family = hp->h_addrtype;
      switch (hp->h_addrtype)
	{
	case AF_INET:
	  memmove (&addr.s_in.sin_addr, hp->h_addr, 4);
	  addr.s_in.sin_port = htons (pnum);
	  break;
	  
	default:
	  grecs_error (NULL, 0,
		       _("%s: invalid connection type: "
			 "unsupported address family"),
		       url->string);
	  return NULL;
	}
    }
  else
    {
      grecs_error (NULL, 0, _("%s: unsupported protocol"), url->string);
      return NULL;
    }

  fd = socket (addr.s.sa_family, SOCK_STREAM, 0);
  if (fd == -1)
    {
      grecs_error (NULL, errno, _("%s: %s failed"), url->string, "socket");
      return NULL;
    }
  
  if ((flags = fcntl (fd, F_GETFD, 0)) == -1
      || fcntl (fd, F_SETFD, flags | FD_CLOEXEC) == -1)
    grecs_error (NULL, 0,  _("%s: cannot set close-on-exec: %s"),
		 url->string, strerror (errno));

  if (source_addr)
    {
      if (source_addr->sa->sa_family != addr.s.sa_family)
	grecs_error (NULL, 0,
		     _("source and destination address family differ"));
      else if (bind (fd, source_addr->sa, source_addr->len) < 0)
	{
	  grecs_error (NULL, errno, _("%s: %s failed"), url->string, "bind");
	  exit (EX_UNAVAILABLE);
	}
    }
  
  if (connect (fd, &addr.s, socklen))
    {
      grecs_error (NULL, errno, _("%s: %s failed"), url->string, "connect");
      close (fd);
      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;
  if (pies_url_copy (&conn->url, url))
    grecs_alloc_die ();

  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
	    {
	      conn->result = json_parse_string (conn->resp.content,
						    conn->resp.content_length);
	      if (!conn->result)
		{
		  grecs_error (&json_err_locus, 0,
			       "%s", json_err_diag);
		  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_format_value (v, &fmt);
  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, "error_message")))
    {
      if (jv->type == json_string)
	grecs_error (NULL, 0, "%s", jv->v.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;

  switch (json_object_get (conn->result, name, &p))
    {
    case 0:
      if (p->type != type)
	{
	  grecs_error (NULL, 0, _("\"%s\" has wrong type"), name);
	  p = NULL;
	}
      break;

    case 1:
      grecs_error (NULL, 0, _("no \"%s\" member"), name);
      break;
      
    default:
      grecs_error (NULL, errno, _("can't get value of \"%s\""), name);
    }
  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;

  switch (json_object_get (v, name, &p))
    {
    case 0:
      if (p->type != type)
	{
	  grecs_error (NULL, 0, _("\"%s\" has wrong type"), name);
	  p = NULL;
	}
      break;

    case 1:
      if (!optional)
	grecs_error (NULL, 0, _("no \"%s\" member"), name);
      break;
      
    default:
	grecs_error (NULL, errno, _("can't get value of \"%s\""), name);
    }
  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->v.s;

  p = getval (v, "tag", json_string, 0);
  if (!p)
    {
      print_json (fp, v);
      return;
    }
	  
  fprintf (fp, "%-16s ", p->v.s);

  if (strcmp (type, "component") == 0)
    {
      p = getval (v, "mode", json_string, 0);
      if (p)
	fbuf[fidx++] = kwtoc (p->v.s, mode_trans);
      else
	fbuf[fidx++] = '-';
      p = getval (v, "active", json_bool, 0);
      if (p && p->v.b)
	{
	  p = getval (v, "status", json_string, 0);
	  if (p)
	    fbuf[fidx++] = status = kwtoc (p->v.s, status_trans);
	  else
	    fbuf[fidx++] = '-';
	}
      else
      	fbuf[fidx++] = '-';
    }
  else if (strcmp (type, "redirector") == 0)
    {
      fbuf[fidx++] = 'R';
    }
  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->v.n);
      else if (status == 'L' /* listener */
	       && (p = getval (v, "URL", json_string, 1)))
	fprintf (fp, "%-10s ", p->v.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->v.n;
	      fprintftime (fp, "%H:%M:%S", localtime (&t), 0, 0);
	    }
	}
      
      p = getval (v, "argv", json_arr, 0);
      if (p)
	{
	  size_t i, n = json_array_size (p);
	  
	  for (i = 0; i < n; i++)
	    {
	      struct json_value *elt;
	      if (json_array_get (p, i, &elt) == 0)
		fprintf (fp, " %s", elt->v.s);
	    }
	}
    }
  else if (strcmp (type, "redirector") == 0)
    {
      p = getval (v, "PID", json_number, 0);
      if (p)
	fprintf (fp, "%10.0f ", p->v.n);
    }
  else if (strcmp (type, "command") == 0)
    {
      p = getval (v, "command", json_string, 0);
      if (p)
	fprintf (fp, "%s", p->v.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_format_value (val, &fmt);
    }
  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 parentesis"));
    }
  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->v.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_size (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_arr)
    {
      size_t i, n = json_array_size (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_arr)
    {
      size_t i, n = json_array_size (conn->result);
      
      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->v.s);
		  if (json_object_get_type (elt, "status", json_string, &v)
		      == 0)
		    {
		      if (strcmp (v->v.s, "OK") == 0)
			fputs (v->v.s, stdout);
		      else if (json_object_get_type (elt, "error_message",
						     json_string, &v) == 0)
			fputs (v->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->v.s, stdout);
}

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

static void
id_fmt_argv (struct json_value *val)
{
  size_t i, n;
  char const *delim = NULL;
  
  n = json_array_size (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->v.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_arr,    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_arr:
      {
	size_t i, n;

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

    case json_object:
      {
	struct json_value *val;
	
	val = json_object_require_type (conn->result, "status", json_string);
	if (strcmp (val->v.s, "OK") == 0)
	  {
	    if (json_object_get_type (conn->result, "value",
				      json_string, &val) == 0)
	      printf ("%s\n", val->v.s);
	  }
	else if (strcmp (val->v.s, "ER") == 0)
	  {
	    if (json_object_get_type (conn->result, "error_message",
				      json_string, &val) == 0)
	      fputs (val->v.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_format_value (parser.query, &fmt);
	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_arr, &tmp) == 0
      && json_array_size (tmp) == 2)
    {
      if (json_array_get (tmp, 0, &x) == 0 && x->type == json_string)
	loc->beg.file = x->v.s;
      else
	loc->beg.file = "<unknown>";
      if (json_array_get (tmp, 1, &x) == 0 && x->type == json_string)
	loc->end.file = x->v.s;
      else
	loc->beg.file = "<unknown>";
    }

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

  if (json_object_get_type (msg, "col", json_arr, &tmp) == 0
      && json_array_size (tmp) == 2)
    {
      if (json_array_get (tmp, 0, &x) == 0 && x->type == json_number)
	loc->beg.col = x->v.n;
      if (json_array_get (tmp, 1, &x) == 0 && x->type == json_number)
	loc->end.col = x->v.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->v.s, "ER") == 0)
    {
      if (json_object_get_type (conn->result, "error_message",
				json_string, &val) == 0)
	fputs (val->v.s, stdout);
      else
	printf ("%s", _("unknown error"));
      fputc ('\n', stdout);
    }

  if (json_object_get_type (conn->result, "parser_messages", json_arr,
			    &val) == 0)
    {
      size_t i, n = json_array_size (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->v.b;
	  if (json_object_get_type (msg, "syserrno", json_number, &tmp) == 0)
	    ec = tmp->v.n;
	  
	  tmp = json_object_require_type (msg, "message", json_string);
	  piesctl_diag (&loc, err, ec, tmp->v.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_arr);
  n = json_array_size (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->v.s;
      if (json_object_get_type (val, "file", json_string, &tmp))
	continue;
      file = tmp->v.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->v.s, "OK"))
    {
      if (json_object_get_type (conn->result, "error_message",
				json_string, &val) == 0)
	fputs (val->v.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->v.s, "OK") == 0)
    {
      if (json_object_get_type (conn->result, "error_message",
				json_string, &val) == 0)
	{
	  fputs (val->v.s, stderr);
	  fputc ('\n', stderr);
	}
      if (json_object_get_type (conn->result, "result", json_arr, &val) == 0)
	{
	  size_t i, n = json_array_size (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->v.b)
		{
		  fputs (save_state.argv[i], stderr);
		  fputc ('\n', stderr);
		}
	    }
	}
    }
  else
    {
      if (json_object_get_type (conn->result, "error_message",
				json_string, &val) == 0)
	fputs (val->v.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\
    <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\
                    \"_\" | \".\" | \"*\" | \":\" | \"@\" | \"[\" | \"]\" | \"-\" | \"/\"\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 (FILE *fp)
{
  struct comtab *cp;
  char const *command = NULL;
  
  fputs (_("Available commands with their arguments are:"), fp);
  fputc ('\n', fp);

  for (cp = comtab; !comtabend (cp); cp++)
    {
      if (cp->argdoc)
	{
	  if (cp->name)
	    {
	      fputc ('\n', fp);
	      command = cp->name;
	    }
	  fputs (command, fp);
	  if (cp->argdoc[0])
	    {
	      fputc (' ', fp);
	      fputs (gettext (cp->argdoc), fp);
	    }
	}
      fputc ('\n', fp);
      if (cp->docstr[0])
	fprintf (fp, "  %s\n", gettext (cp->docstr));
    }
  fputc ('\n', fp);
}

int
main (int argc, char **argv)
{
  int i;
  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;

  proginfo.print_help_hook = command_help;
  parse_options (argc, argv, &i);
  argc -= i;
  argv += i;
  if (argc == 0)
    parse_error ("%s", _("not enough arguments"));
  
  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;
}
