import click
import logging
import tarfile
import json
import hashlib
import subprocess
from pathlib import Path
from rich.pretty import pprint
from rich.logging import RichHandler
from jinja2 import (Environment, PackageLoader, StrictUndefined,
                    select_autoescape)
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from base64 import b64decode
from pymoku.parts import include


FORMAT = "%(message)s"
logging.basicConfig(
    level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
)

log = logging.getLogger()


PLATFORMS = {  # TODO these could be parsed from part.defs
    'mokugo': [1, 2],
    'mokulab': [1, 2],
    'mokupro': [1, 4, 8],
    'mokudelta': [1, 4, 8],
}

templates = Path(__file__).parent / 'templates'
jenv = Environment(
    loader=PackageLoader('pymoku.sdk'),
    autoescape=select_autoescape(),
    undefined=StrictUndefined,
    keep_trailing_newline=True,
    trim_blocks=True,
)


def create_parts_def(path, name, namespace):
    path = path / 'parts.def'
    log.info(f"Create '{path}'")

    if path.exists():
        log.warning(f"'{path}' already exists, skipping")
        return

    tmpl = jenv.get_template('parts.def')
    text = tmpl.render(name=name.lower(),
                       namespace=namespace.lower(),
                       top_entity=f'{name}Slot')
    path.write_text(text)

    if (parts_path := Path('parts.def')).exists():
        include_text = f"include('{name.lower()}', prefix='{name.lower()}')"
        log.info(f'adding {include_text} to {parts_path}')
        with parts_path.open('a') as f:
            f.write(f"{include_text}\n")


def create_vhd_sources(path, name):
    top_entity = f'{name}Slot'
    path = path / f'{top_entity}.vhd'
    log.info(f"Create '{path}'")

    path.parent.mkdir(parents=True, exist_ok=True)

    if path.exists():
        log.warning(f"'{path}' already exists, skipping")
        return

    tmpl = jenv.get_template('Top.vhd')
    text = tmpl.render(top_entity=top_entity)
    path.write_text(text)


def append_platform(path, name, namespace, platform):
    path = path / 'parts.def'
    log.info(f"Adding {platform} to '{path}'")

    if not path.exists():
        log.error(f"'{path}' doesn't exist")
        return

    tmpl = jenv.get_template('parts_platform.def')
    text = tmpl.render(top_entity=f'{name}Slot',
                       name=name.lower(),
                       namespace=namespace.lower(),
                       hw=platform,
                       platforms=PLATFORMS[platform])

    with path.open('a') as f:
        f.write(text)


def create_slot_test(path, name):
    top_entity = f'{name}Slot'
    top_path = path / f'Test{top_entity}.vhd'
    log.info(f"Create '{top_path}'")

    top_path.parent.mkdir(parents=True, exist_ok=True)

    if not top_path.exists():
        tmpl = jenv.get_template('TestTop.vhd')
        text = tmpl.render(top_entity=top_entity, num_ain=4, num_aout=4)
        top_path.write_text(text)
    else:
        log.warning(f"'{top_path}' already exists, skipping")

    mod_path = path / f'test_{name.lower()}.py'
    log.info(f"Create '{mod_path}'")

    if not mod_path.exists():
        tmpl = jenv.get_template('test_module.py')
        text = tmpl.render(name=name.lower(), top_entity=top_entity, num_ain=4, num_aout=4)
        mod_path.write_text(text)
    else:
        log.warning(f"'{mod_path}' already exists, skipping")


def create_python_module(path, name):
    top_entity = f'{name}Slot'
    mod_path = path / f'{name.lower()}.py'
    log.info(f"Create '{mod_path}'")

    if not mod_path.exists():
        mod_path.parent.mkdir(parents=True, exist_ok=True)
        tmpl = jenv.get_template('module.py')
        text = tmpl.render(name=name, top_entity=top_entity)
        mod_path.write_text(text)
    else:
        log.warning(f"'{mod_path}' already exists, skipping")

    init_path = path / '__init__.py'
    log.info(f"Create '{init_path}'")

    if not init_path.exists():
        tmpl = jenv.get_template('plugin.py')
        text = tmpl.render(name=name, top_entity=top_entity)
        init_path.write_text(text)
    else:
        log.warning(f"'{init_path}' already exists, skipping")

    applet_path = path / 'applet.py'
    log.info(f"Create '{applet_path}'")

    if not applet_path.exists():
        tmpl = jenv.get_template('applet.py')
        text = tmpl.render(name=name)
        applet_path.write_text(text)
    else:
        log.warning(f"'{applet_path}' already exists, skipping")

    api_path = path / 'api.py'
    log.info(f"Create '{api_path}'")

    if not api_path.exists():
        tmpl = jenv.get_template('api.py')
        text = tmpl.render(name=name)
        api_path.write_text(text)
    else:
        log.warning(f"'{api_path}' already exists, skipping")


def init_git(path, name):
    gitignore_path = path / '.gitignore'
    log.info("Initializing Git repository")

    if not gitignore_path.exists():
        tmpl = jenv.get_template('gitignore')
        text = tmpl.render()
        gitignore_path.write_text(text)
    else:
        log.warning(f"'{gitignore_path}' already exists, skipping")

    subprocess.run(['git', 'init', '.'], cwd=path, capture_output=True)
    subprocess.run(['git', 'add', '-A'], cwd=path, capture_output=True)
    subprocess.run(['git', 'commit', '-m', f"Initial creation of {name}"], cwd=path, capture_output=True)


@click.group()
@click.version_option(package_name='pymoku.sdk')
def cli():
    pass


@cli.command()
@click.argument('name')
@click.option('--namespace', default='custom')
@click.option('--hw', help=f"Target hardware platform {list(PLATFORMS.keys())}")
@click.option('--git', is_flag=True, help="Initialize Git repository")
def new(name, namespace='custom', hw=None, git=False):
    path = Path('.') / name.lower()
    log.info(f"Creating new project '{path}'")
    path.mkdir(parents=True, exist_ok=True)

    create_parts_def(path, name, namespace)
    create_vhd_sources(path / 'vhdl', name)
    create_python_module(path / 'python', name)
    create_slot_test(path / 'tests', name)

    if hw:
        append_platform(path, name, namespace, hw)

    if git:
        init_git(path, name)

    log.info('Finished')


@cli.command()
@click.argument('name')
@click.argument('hw')
def add_platform(name, hw):
    path = Path('.') / name.lower()
    append_platform(path, name, hw)


@cli.command()
@click.argument('name')
def build(name):
    build_targets = include(Path(name) / 'parts.def')
    print(build_targets)


from pymoku.sdk import keys  # noqa


@cli.command()
@click.argument('barfile', type=click.Path(exists=True, dir_okay=False))
@click.option('--raw', is_flag=True, help="Don't decode or pretty-print")
def manifest(barfile, raw=False):
    """ Print the manifest from the specified barfile. """
    with tarfile.open(barfile, mode='r') as tf:
        manifest = tf.extractfile('MANIFEST')
        if raw:
            print(manifest.read().decode())
        else:
            pprint(json.load(manifest))


@cli.command()
@click.argument('barfile', type=click.Path(exists=True, dir_okay=False))
def verify(barfile):
    """ Verify all manifest signatures available and all file hashes of the
        specified barfile.
   """
    with tarfile.open(barfile, mode='r') as tf:
        manifest_bytes = tf.extractfile('MANIFEST').read()
        manifest = json.loads(manifest_bytes.decode())

        # check all the signatures match
        for keyname, pubkey_b64 in keys.list_public_keys():
            pubkey = Ed25519PublicKey.from_public_bytes(b64decode(pubkey_b64))
            try:
                sig_bytes = tf.extractfile(f'MANIFEST.{keyname}.sig').read()
                pubkey.verify(sig_bytes, manifest_bytes)
                log.info(f"Signature matches '{keyname}'")
            except KeyError:
                log.debug(f"no signature for '{keyname}'")
            except InvalidSignature:
                log.error(f"Invalid signature for '{keyname}'")

        # check all the file hashes
        for item in manifest['items']:
            if (fname := item.get('filename')) is None:
                continue

            if (manifest_sha := item.get('sha256')) is None:
                continue

            log.debug(f"Checking hash for '{item.get('_id')}:{fname}'")
            try:
                file_sha = hashlib.sha256(tf.extractfile(fname).read())
                if file_sha.hexdigest() != manifest_sha:
                    log.error(f"Hash does not match for '{item.get('_id')}'")
            except KeyError:
                log.error(f"Referenced file '{item.get('_id')}:{fname}' not in archive")


if __name__ == '__main__':
    cli()
