"""Manage package creation, building, and submitting.

The package module provides a set of utilities designed for package developers
to create packages, build packages from python source code, and submit packages
to be included in the list of installable packages within Area28.
"""
import hashlib
import json
import os
import shutil
import sys
from argparse import Namespace
from a28 import log, utils
from a28.api import API, find_or_create_package
from a28.build import add_build_parser, extract_meta
from a28.config import ConfigFile
from a28.publish import add_publish_parser


package_types = [
    'api', 'app', 'chat', 'event',
    'interface', 'logger', 'metadata',
    'preference', 'realtime', 'repository',
    'ui', 'units'
]


def cli_options(main_parser):
    """Argparse options added to the cli."""
    parser = main_parser.add_parser(
        'package',
        aliases=['pkg'],
        help='package actions'
    )
    package_parser = parser.add_subparsers(
        dest='package',
        required=True,
        help='package',
    )
    add_init_parser(package_parser.add_parser(
        'init',
        help='Initialize a package',
    ))
    add_build_parser(package_parser.add_parser(
        'build',
        help='build package',
    ))
    add_install_parser(package_parser.add_parser(
        'install',
        help='install package',
    ))
    add_publish_parser(package_parser.add_parser(
        'publish',
        help='publish a package',
    ))


def add_init_parser(parser_init):
    """Create the CLI parser for 'init' command."""
    parser_init.set_defaults(func=initialize)
    parser_init.add_argument(
        'path',
        default='.',
        help="package path default '.'",
    )
    parser_init.add_argument(
        '-s',
        '--scope',
        required=True,
        help='package scope eg. group-name',
    )
    parser_init.add_argument(
        '-n',
        '--name',
        required=True,
        help='package name eg. pkg-name',
    )
    parser_init.add_argument(
        '-t',
        '--type',
        required=True,
        choices=package_types,
        help='package type',
    )
    parser_init.add_argument(
        '-i',
        '--identifier',
        required=False,
        help='provide a pre-registered identifier (defaults to getting one from API)'
    )
    parser_init.add_argument(
        '--bin',
        action='store_true',
        help='create the bin directory',
    )
    parser_init.add_argument(
        '--script',
        action='store_true',
        help='create the scripts directory',
    )
    parser_init.add_argument(
        '-f',
        '--force',
        action='store_true',
        help='force overwriting package.json',
    )
    parser_init.add_argument(
        '-e',
        '--endpoint',
        default=API.DEFAULT_ENDPOINT,
        help='endpoint to use.',
    )


def add_install_parser(parser_install):
    """Create the CLI parser for 'install' command."""
    parser_install.set_defaults(func=install)
    parser_install.add_argument(
        '--pkg',
        required=True,
        help='package a28 package file',
    )


def generate_sub_dirs(args):
    """Generate a list of sub directories from the give args."""
    sub_dirs = ['extensions', 'plugins']

    if args.bin:
        log.debug('adding bin')
        sub_dirs.append('bin')
    if args.script:
        log.debug('adding scripts')
        sub_dirs.append('script')
    if args.type == 'application':
        sub_dirs.append('plugin')

    return sub_dirs


def generate_jsondata(scope, name, schema, identifier, args):
    """Generate a basic package.json data."""
    json_data = {
        'name': f'@{scope}/{name}',
        'description': f'{schema} package created using the A28 command by {scope}.',
        'version': '0.1.0',
        'identifier': identifier
    }

    if args.bin:
        json_data['bin'] = {}
    if args.script:
        json_data['scripts'] = {}

    return json_data


def initialize(args: Namespace):
    """Initialize a package in a specified directory."""
    (path, name, scope, schema) = _get_init_args(args)
    log.info(f'initializing @{scope}/{name} in folder "{path}"')

    # check if the user has authenticated
    if not ConfigFile.load():
        utils.message('authenticate before initializing')
        sys.exit(1)

    identifier = find_or_create_package(
        region=args.endpoint,
        identifier=args.identifier,
        schema=schema,
        scope=scope,
        name=name
    )

    if not identifier:
        sys.exit(1)

    if not os.path.exists(path):
        os.makedirs(path, exist_ok=True)

    json_file = os.path.join(path, 'package.json')

    _build_structure(path, generate_sub_dirs(args))
    if not os.path.isfile(json_file) or args.force or utils.confirm('overwrite package.json?'):
        log.info('creating package.json')
        with open(json_file, 'w') as pkg_file:
            json.dump(generate_jsondata(scope, name, schema, identifier, args), pkg_file, indent=4)
    else:
        log.info('not overwriting package.json')
        return

    utils.message('package created')


def install(args):
    """Install / update the local package."""
    pkg_hash = generate_hash(args.pkg)
    meta = extract_meta(args.pkg)
    meta['hash'] = pkg_hash
    install_local(args.pkg, meta)
    utils.message('installed {}'.format(meta['name']))


def install_local(pkg, meta):
    """Install a package locally by calling the update_index function."""
    dest = os.path.join(utils.STORAGE, 'cache')
    shutil.copy(pkg, dest)
    index = os.path.join(utils.STORAGE, 'index.json')
    with open(index, 'r+') as index_data:
        update_index(index_data, meta)


def update_index(index_data, meta):
    """
    Update the index.json without touching the etag.json file.

    This will be overwritten when a new official index.json is published.
    """
    data = json.load(index_data)
    data['packages'][meta['name']] = meta
    if meta['name'] not in data['required']:
        data['required'].append(meta['name'])
    index_data.seek(0)
    json.dump(data, index_data, indent=4)
    index_data.truncate()


def generate_hash(pkg):
    """Generate a hash for a package."""
    sha256_hash = hashlib.sha256()
    with open(pkg, 'rb') as f:
        # read and update hash string value in blocks of 4K
        for byte_block in iter(lambda: f.read(4096), b''):
            sha256_hash.update(byte_block)
    return sha256_hash.hexdigest()


def _get_init_args(args):
    path = os.path.realpath(args.path)
    if not utils.valid_string(args.name):
        raise ValueError(f'{args.name} is not a valid name')
    name = args.name.lower()
    if not utils.valid_string(args.scope):
        raise ValueError(f'{args.scope} is not a valid scope')
    scope = args.scope.lower()
    if not utils.valid_string(args.type, min_length=2):
        raise ValueError(f'{args.type} is not a valid name')
    schema = args.type.lower()
    return path, name, scope, schema


def _build_structure(path, subdirs):
    for subdir in subdirs:
        os.makedirs(os.path.join(path, subdir), exist_ok=True)
