#define _GNU_SOURCE

#if HAVE_CONFIG_H
# include "config.h"
#endif

#include <stdarg.h>
#include <stdio.h>
#include <string.h>

#include <errno.h>
#include <getopt.h>
#include <unistd.h>

#include "ascii.h"
#include "crypto-internal.h"
#include "proof.h"
#include "ssh.h"
#include "xalloc.h"

static void __attribute__((noreturn))
die (const char *format, ...)
{
  va_list args;
  va_start (args, format);
  vfprintf (stderr, format, args);
  va_end (args);
  fprintf (stderr, "\n");
  exit (1);
}

#define MAX_LINE 1000
#define MAX_PROOF 10000
#define MAX_POLICY 10000

DEFINE_LIST(pubkey_list, pubkey_alloc, struct sigsum_key_with_hash);

static int
key_hash_cmp (const void *ap, const void *bp)
{
  const struct sigsum_key_with_hash *a = ap;
  const struct sigsum_key_with_hash *b = bp;
  return memcmp (a->hash.h, b->hash.h, sizeof (a->hash.h));
}

static void
read_keys (struct sigsum_key_map *map, const char *key_file)
{
  struct pubkey_list keys = { 0, 0, NULL };
  FILE *f = fopen (key_file, "r");
  if (!f)
    die ("Failed to open file %s: %s", key_file, strerror (errno));

  char line[MAX_LINE];
  const char *s;
  size_t lineno = 0;
  /* Read openssh public key files, processing Ed25519 lines only.
     TODO: Recognize attributes, e.g., sigsum-policy=...*/
  while ((s = fgets (line, sizeof (line), f)) != NULL)
    {
      lineno++;
      s += strspn (s, " \t");
      if (*s == '#')
	continue;

      struct sigsum_key_with_hash *entry = pubkey_alloc (&keys);
      const char *err;
      if (!sigsum_ssh_parse_public_key (&entry->key, s, &err))
	die ("Invalid key, %s:%zu: %s", key_file, lineno, err);
      sigsum_hash_key (&entry->hash, &entry->key);
    }
  if (ferror (f))
    die ("Reading %s failed: %s", key_file, strerror (errno));

  fclose (f);

  if (!keys.size)
    die ("No ed25519 keys: %s", key_file);

  qsort (keys.list, keys.size, sizeof (keys.list[0]), key_hash_cmp);
  map->num_keys = keys.size;
  map->keys = keys.list;
}

static size_t
read_file (size_t buf_size, char *data, const char *file_name)
{
  FILE *f = fopen (file_name, "r");
  if (!f)
    die ("Failed to open file %s: %s", file_name, strerror (errno));

  size_t res = fread (data, 1, buf_size, f);
  if (res == buf_size)
    {
      /* Ensure that we have read to EOF. */
      char c;
      if (fread (&c, 1, 1, f))
	die ("File %s too large", file_name);
    }
  if (ferror (f))
    die ("Reading %s failed: %s", file_name, strerror (errno));

  fclose (f);
  return res;
}

static void
read_proof (struct sigsum_proof *proof, const char *proof_file)
{
  char data[MAX_PROOF];
  size_t size = read_file (sizeof (data), data, proof_file);
  const char *err;
  if (!sigsum_proof_parse (proof, size, data, &err))
    die ("Parsing proof file %s failed: %s", proof_file, err);
}

static void
read_policy (struct sigsum_policy *policy, const char *policy_file)
{
  char data[MAX_POLICY];
  size_t size = read_file (sizeof (data), data, policy_file);
  struct sigsum_error err;
  if (!sigsum_policy_parse (policy, size, data,  &err))
    die ("Parsing policy file %s:%zu failed: %s", policy_file, err.line, err.msg);
}

static void
read_message (struct sigsum_hash *message)
{
  struct sigsum_hash_ctx ctx;
  _sigsum_hash_init (&ctx);
  for (;;)
    {
      uint8_t buf[10000];
      size_t size = fread (buf, 1, sizeof (buf), stdin);
      _sigsum_hash_update (&ctx, size, buf);
      if (size < sizeof (buf))
	break;
    }
  if (ferror (stdin))
    die ("Reading message on stdin failed: %s", strerror (errno));

  _sigsum_hash_digest (&ctx, message);
}

/* Maximum size for "raw hash":
   - 64 hex digits for the hash
   - 2 chars for "\r\n"
   - 1 extra char for null terminator / overflow detection */
#define MAX_HASH 67

static void
read_raw_hash (struct sigsum_hash *message)
{
  char in[MAX_HASH];
  size_t size = fread (in, 1, sizeof(in), stdin);

  if (size == sizeof (in))
    die ("Input too large");
  if (ferror (stdin))
    die ("Reading message on stdin failed: %s", strerror (errno));
  if (size == SIGSUM_DIGEST_SIZE)
    {
      memcpy (message->h, in, SIGSUM_DIGEST_SIZE);
      return;
    }
  /* Strip at most one trailing CRLF or LF */
  if (size > 0 && in[size - 1] == '\n')
    {
      size--;
      if (size > 0 && in[size - 1] == '\r')
	size--;
    }
  if (size != 64)
    die ("Invalid size (%zu) for raw message hash, expected a line with 64 characters",
         size);
  if (!_sigsum_decode_hex_hash (message, in))
    die ("Invalid hex value for raw message hash");
}

#define OPT_HELP -2
#define OPT_RAW -3

int
main (int argc, char **argv)
{
  static const struct option options[] = {
    { "raw-hash", no_argument, NULL, OPT_RAW },
    { "key", required_argument, NULL, 'k' },
    { "policy", required_argument, NULL, 'p' },
    /* TODO: Add -P / --named-policy */
    { "help", no_argument, NULL, OPT_HELP },
    { "version", no_argument, NULL, 'v' },
    { NULL, 0, NULL, 0 },
  };
  int c;

  bool raw_flag = false;
  const char *key_file = NULL;
  const char *policy_file = NULL;

  const char *program_name = argv[0];

  while ((c = getopt_long (argc, argv, "k:p:v", options, NULL)) != -1)
    switch (c)
      {
      case 'k':
	if (key_file)
	  die ("Only one key file (-k) can be specified.");

	key_file = optarg;
	break;

      case 'p':
	if (policy_file)
	  die ("Only one policy file (-p) can be specified.");

	policy_file = optarg;
	break;

      case OPT_RAW:
	raw_flag = true;
	break;

      case 'v':
	printf ("sigsum-c-verify (%s) %s\n", PACKAGE_NAME, PACKAGE_VERSION);
	exit (0);

      case OPT_HELP:
	printf ("Usage: sigsum-c-verify [options] proof-file\n"
		"Verify that a message's signed checksum is logged,\n"
		"according to a given trust policy.\n\n"
		"Options:\n"
		"  -k, --key=FILE    submitter key(s) in openssh format\n"
		"  -p, --policy=FILE sigsum policy file\n"
		"      --raw-hash    input is the message hash,\n"
		"                    represented as either 32 binary octets\n"
		"                    or 64 hex digits, followed by an\n"
		"                    optional newline.\n"
		"  -v, --version     display version info\n"
		"      --help        display this help\n");

	exit (0);

      default:
	die ("Invalid arguments, see %s --help.", program_name);
    }
  if (!key_file)
    die ("Missing key file (-k option), see also %s --help.", program_name);

  if (!policy_file)
    die ("Missing policy file (-p option), see also %s --help.", program_name);

  argc -= optind; argv += optind;
  if (argc < 1)
    die ("Missing proof argument, see also %s --help.", program_name);

  if (argc > 1)
    die ("Too many arguments, see also %s --help.", program_name);

  struct sigsum_proof proof;
  read_proof (&proof, argv[0]);

  struct sigsum_hash message;
  if (raw_flag)
    read_raw_hash (&message);
  else
    read_message (&message);

  struct sigsum_policy policy;
  read_policy (&policy, policy_file);

  struct sigsum_key_map submitters;
  read_keys (&submitters, key_file);

  bool *scratch = _sigsum_xalloc (sigsum_policy_verify_tree_head_itch (&policy));
  const char *err;

  if (!sigsum_proof_verify (&message, &proof,
			    &submitters, &policy, &err, scratch))
    die ("Invalid: %s", err);

  sigsum_policy_clear (&policy);
  sigsum_proof_clear (&proof);
  free ((void *) submitters.keys);
  free (scratch);

  return 0;
}
