#!/usr/bin/python3
# Copyright (C) 2012 Peter Todd <pete@petertodd.org>
#
# This file is part of the OpenTimestamps Client.
#
# It is subject to the license terms in the LICENSE file found in the top-level
# directory of this distribution and at http://opentimestamps.org
#
# No part of the OpenTimestamps Client, including this file, may be copied,
# modified, propagated, or distributed except according to the terms contained
# in the LICENSE file.

import argparse
import binascii
import datetime
import hashlib
import io
import json
import logging
import os
import sys

import opentimestamps

from opentimestamps._internal import FileManager
from opentimestamps.crypto import random_bytes
from opentimestamps.dag import *
from opentimestamps.io import *
from opentimestamps.notary import *
from opentimestamps.rpc import *

import opentimestamps.client
context = None

parser = argparse.ArgumentParser(description="OpenTimestamps client.")

def submit_command(args):
    if args.timestamp is None:
        if args.file is not '-' and not args.digest:
            args.timestamp = args.file + '.ots'
        else:
            parser.error("Must specify timestamp file (or stdout) if input is stdin or raw digest")

    if args.digest:
        import binascii
        try:
            digest = unhexlify(args.file)
        except binascii.Error as err:
            parser.error("Digest must be specified in hex notation: {}".format(str(err)))

        args.file = '-'

    else:
        # Don't let the user overwrite the input
        if os.path.realpath(args.file) == os.path.realpath(args.timestamp):
            parser.error("timestamp would overwrite file")

    with FileManager(args.file,'-') as file_fm:
        # FIXME: handle extending timestamps, IE, timestamp file already exists
        # look in v0.1 git history for how
        assert not os.path.exists(args.timestamp)

        with FileManager('-',args.timestamp) as stamp_fm:
            timestamp = None

            timestamp = TimestampFile(data_fd=file_fm.in_fd, out_fd=stamp_fm.out_fd)
            logging.info("Creating a new timestamp file")

            if args.digest:
                timestamp.digests['none'] = digest
            else:
                timestamp.add_algorithms('sha256d')
                digest = timestamp.digests['sha256d']

            new_ops = []
            if not args.no_nonce:
                nonce = random_bytes(32)
                hash_op = Hash(digest,nonce,parents=(digest,))
                new_ops.append(hash_op)
                digest = hash_op

            server = OtsServer(args.server)

            reply_ops = server.post_digest(digest=hexlify(digest))
            reply_ops = [Op.from_primitives(op) for op in reply_ops]

            new_ops.extend(reply_ops)
            timestamp.dag.update(new_ops)

            timestamp.write()

            stamp_fm.commit()

            logging.debug("New ops:\n" +
                    json.dumps([op.to_primitives() for op in new_ops],indent=4))


def sign_command(args):
    raise NotImplementedError

    server = OtsServer(args.server)

    notary = PGPNotary(identity=args.fingerprint)
    notary.canonicalize_identity()

    merkle_tip_ops = server.get_merkle_tip()
    logging.debug(json.dumps(json_serialize(merkle_tip_ops),indent=4))

    sig = notary.sign(merkle_tip_ops[-1].digest,int(time.time()*1000000))

    verify_op = Verify(inputs=(merkle_tip_ops[-1],),signature=sig)
    merkle_tip_ops.append(verify_op)
    r = server.post_verification(ops=merkle_tip_ops)


    logging.debug(json.dumps(json_serialize(r),indent=4))
    logging.info('Signed digest %s for server %s' %
            (binascii.hexlify(merkle_tip_ops[-2].digest),server.url))


def complete_command(args):
    with FileManager(args.timestamp,args.output) as fm:
        timestamp_file = TimestampFile(in_fd=fm.in_fd,out_fd=fm.out_fd)

        new_sigs = []
        new_ops = []
        old_ops = set(timestamp_file.dag)

        server = OtsServer(args.server)

        for op in timestamp_file.dag:
            if args.server in op.metadata:
                (ops,sigs) = server.get_path(hexlify(op), args.notary)
                ops = [Op.from_primitives(op) for op in ops]
                sigs = [Signature.from_primitives(sig) for sig in sigs]
                new_sigs.extend(sigs)
                new_ops.extend(ops)

        timestamp_file.dag.update(new_ops)
        timestamp_file.signatures.update(new_sigs)

        if new_ops or new_sigs:
            timestamp_file.write()
            fm.commit()

            new_ops = set(new_ops).difference(old_ops)
            logging.debug("Got new ops from server:\n %s",
                    json.dumps([Op.to_primitives(op) for op in new_ops], indent=4))

            new_sigs = set(new_sigs).difference(timestamp_file.signatures)
            for sig in new_sigs:
                timestamp = datetime.datetime.fromtimestamp(sig.timestamp)
                logging.info('New signature from {}:{} with timestamp {}'.format(
                                  sig.method,
                                  sig.identity,
                                  timestamp.isoformat(' ')))
        else:
            logging.info('No new signatures found.')


def jsondump_command(args):
    with FileManager(args.timestamp,'-') as fm:
        timestamp_file = TimestampFile(in_fd=fm.in_fd,out_fd=None)

        print(json.dumps(timestamp_file.to_primitives(), indent=4))


def verify_command(args):
    data_filename = args.data
    if args.data is None:
        data_filename = '-'
    with FileManager(data_filename,'-') as fm_data,\
         FileManager(args.timestamp,'-') as fm_timestamp:

        data_fd = None
        if args.data is not None:
            data_fd = fm_data.in_fd
        timestamp = TimestampFile(data_fd=data_fd, in_fd=fm_timestamp.in_fd, out_fd=None)

        if args.data is not None:
            timestamp.verify_data()
            print('Data in {} matches timestamp'.format(args.data))
        else:
            print('Data NOT checked')

        timestamp.verify_consistency()

        for sig in timestamp.signatures:
            try:
                sig.verify(context=context)
            except SignatureVerificationError:
                # FIXME: need more detail here...
                pass
            else:
                # FIXME: need better time formatting
                print('GOOD signature from {}:{} with timestamp {}'.format(
                    sig.method, sig.identity,
                    datetime.datetime.fromtimestamp(sig.timestamp)))


def getsourcecode_command(args):
    server = OtsServer(args.server)

    sourcecode_url = server.get_sourcecode()

    print("%s - %s" % (args.server,sourcecode_url))


parser.add_argument("--version",action="version",version=opentimestamps.implementation_identifier)
parser.add_argument("-q","--quiet",action="count",default=0,
                             help="Be more quiet.")
parser.add_argument("-v","--verbose",action="count",default=0,
                             help="Be more verbose. Both -v and -q may be used multiple times.")
parser.add_argument("-c","--config",action="store",default="~/.opentimestamps/config",
        help="Location of config file. Defaults to: ~/.opentimestamps/config")
parser.add_argument("-s","--server",action="store",default="http://pool.opentimestamps.org",
        help="Specify the server. Defaults to: http://pool.opentimestamps.org")

subparsers = parser.add_subparsers(title='Subcommands',
                                   description='All operations are done through subcommands:')

# ----- sign -----
parser_sign = subparsers.add_parser('sign',#aliases=['s'],
        help='Sign a calendar.')
parser_sign.set_defaults(cmd_func=sign_command)
parser_sign.add_argument("fingerprint",action="store",
        help="PGP fingerprint of the signing key")

# ----- submit -----
parser_submit = subparsers.add_parser('submit',#aliases=['s'],
        help='Submit a file to be timestamped.')
parser_submit.set_defaults(cmd_func=submit_command)
parser_submit.add_argument("--no-nonce",action="store_true",default=False,
        help="Don't use a nonce in the hash calculation. Note: this means that"\
        " the server knows what data you have submitted.")
parser_submit.add_argument("-d", "--digest", action="store_true", default=False,
        help="Interpret file as a raw hex digest of hashing a file to generate the digest.")
parser_submit.add_argument("file",action="store",
        help="The file to timestamp. (or - for standard input)")
parser_submit.add_argument("timestamp",action="store",nargs='?',
        help="Optionally specify filename (or - for standard output) to write timestamp to. Required if"\
        " input is standard input. Defaults to <file>.ots")

# ----- complete -----
parser_complete = subparsers.add_parser('complete',#aliases=['x'],
        help='Complete a timestamp.')
parser_complete.set_defaults(cmd_func=complete_command)

parser_complete.add_argument("--notary",action="store",default="*:*",
        help="Notary to complete to")

parser_complete.add_argument("timestamp",action="store",
        help="Filename of timestamp to complete. (or - for standard input)")

parser_complete.add_argument("output",action="store",nargs='?',
        help="Write to this filename (or - for standard output) instead of modifying existing timestamp.")

# ----- jsondump -----
parser_jsondump = subparsers.add_parser('jsondump',#aliases=['x'],
        help='JSON dump a timestamp')
parser_jsondump.set_defaults(cmd_func=jsondump_command)

parser_jsondump.add_argument("timestamp",action="store",
        help="Filename (or - for standard input)")

# ----- verify -----
parser_verify = subparsers.add_parser('verify',#aliases=['v'], aliases not supported in 2.7!
        help="Verify a timestamp")
parser_verify.set_defaults(cmd_func=verify_command)

parser_verify.add_argument("timestamp",action="store",
        help="Filename of timestamp")
parser_verify.add_argument("data",action="store",nargs='?',
        help="Optional data to verify (or - for standard input)")

# ----- getsourcecode -----
parser_getsourcecode = subparsers.add_parser('getsourcecode',
        help='Ask the specified server(s) for where to obtain source code. (AGPL license compliance)')
parser_getsourcecode.set_defaults(cmd_func=getsourcecode_command)

args = parser.parse_args()

args.verbosity = args.verbose - args.quiet

if args.verbosity == 0:
    logging.root.setLevel(logging.INFO)
elif args.verbosity > 0:
    logging.root.setLevel(logging.DEBUG)
elif args.verbosity == -1:
    logging.root.setLevel(logging.WARNING)
elif args.verbosity < -1:
    logging.root.setLevel(logging.ERROR)

context = opentimestamps.client.Context(args.config)

args.cmd_func(args)