#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 <nettle/sha2.h>
#include <nettle/version.h>

#include "ascii.h"
#include "proof.h"
#include "key-map.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 MAX_KEY_FILE 10000

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_keys (struct sigsum_key_map *map, const char *key_file)
{
  char data[MAX_KEY_FILE];
  size_t size = read_file (sizeof (data), data, key_file);
  struct sigsum_error err;
  if (!sigsum_key_map_parse_ssh (map, size, data,  &err))
    die ("Parsing key file %s:%zu failed: %s", key_file, err.line, err.msg);
}

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 sha256_ctx ctx;
  sha256_init (&ctx);
  for (;;)
    {
      uint8_t buf[10000];
      size_t size = fread (buf, 1, sizeof (buf), stdin);
      sha256_update (&ctx, size, buf);
      if (size < sizeof (buf))
	break;
    }
  if (ferror (stdin))
    die ("Reading message on stdin failed: %s", strerror (errno));

#if NETTLE_VERSION_MAJOR < 4
  sha256_digest (&ctx, sizeof (message->h), message->h);
#else
  sha256_digest (&ctx, message->h);
#endif
}

/* 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 < input\n"
		"Verify that a message's signed checksum is logged for a given trust\n"
		"policy. The message to be verified is read on stdin.\n\n"
		"Options:\n"
		"  -k, --key=FILE    submitter public key(s), in OpenSSH format\n"
		"  -p, --policy=FILE sigsum trust policy file\n"
		"      --raw-hash    input has already been hashed,\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 = malloc (sigsum_policy_verify_tree_head_itch (&policy));
  if (!scratch)
    die ("Memory exhausted.");

  const char *err;

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

  sigsum_policy_clear (&policy);
  sigsum_proof_clear (&proof);
  sigsum_key_map_clear (&submitters);
  free (scratch);

  return 0;
}
