#!/usr/bin/env python3

import configparser
import getopt
import logging
import os
import re
import sys

from urllib.parse import parse_qs

import libfmp4
import schema
import yaml


def fatal(msg):
    print(msg, file=sys.stderr)
    sys.exit(1)


def attr_name_and_value(obj, name):
    try:
        value = getattr(obj, name)
    except AttributeError:
        return ' ' + name + ': <no such attribute>'
    return ' ' + name + ': ' + repr(value)


def module_report(mod):

    result = "loaded module:"

    result += attr_name_and_value(mod, '__name__')
    result += attr_name_and_value(mod, '__version__')
    result += attr_name_and_value(mod, '__file__')
    result += attr_name_and_value(mod, '__cached__')

    return result


def usage():
    fatal('''Usage: manifest_edit [options] [--python_pipeline_config=<filename>] <input>

 --python_pipeline_path=<dirname>     manifest edit pipeline config path.
 --python_pipeline_config=<filename>  manifest edit pipeline config file.
 <input>                              manifest input file or URL
 [-o outfile]                         output file.
 [-v level]                           0=error 1=warning 2=info 3=debug 4=trace
 [--python_path path]                 manifest edit python modules search path.
                                      If not provided, defaults to the current
                                      system PYTHONPATH plus the current dir.
 --verbose_manifest=<true|false>      option to dump the pipeline configuration
                                      in the manifest as a comment. Set to false
                                      to only add a one-liner comment.
                                      (Default=True).

Please note that the input file specification may need quoting when extra
parameters are appended in URL style.

This program is part of USP (Unified Streaming Platform). For more information
and its license, please visit http://www.unified-streaming.com
''')


def get_default_python_path():

    try:
        this_file = os.path.abspath(sys.modules['__main__'].__file__)
    except AttributeError:
        this_file = sys.executable
    if this_file is None:
        return None

    this_files_dir = os.path.dirname(this_file)
    one_dir_up = os.path.dirname(this_files_dir)
    two_dirs_up = os.path.dirname(one_dir_up)

    if os.path.basename(one_dir_up) == 'usp-python-apps' and \
            os.path.basename(two_dirs_up) == 'libexec':
        etc_prefix_dir = os.path.dirname(two_dirs_up)
        if os.name == 'posix' and etc_prefix_dir == '/usr':
            etc_prefix_dir = '/'
        config_base_path = os.path.join(etc_prefix_dir, 'etc', 'manifest-edit')
    elif os.name == 'posix':
        config_base_path = '/etc/manifest-edit'
    elif os.name == 'nt':
        config_base_path = os.path.join(this_files_dir, 'etc')
    else:
        return None

    cp = configparser.ConfigParser()
    cp.read(os.path.join(config_base_path, 'conf', 'manifest-edit.conf'))
    if 'manifest-edit' in cp and 'default_python_path' in cp['manifest-edit']:
        config_base_path = cp['manifest-edit']['default_python_path']

    return os.path.join(config_base_path, 'plugins')


def get_format_from_yaml(yaml_name):
    try:
        with open(yaml_name, 'r') as yaml_file:
            for line in yaml_file:
                before, sep, after = line.partition(':')
                if sep == ':' and before in ('mpd', 'm3u8_main', 'm3u8_media'):
                    return before
        fatal(f'ERROR: unknown manifest format in {yaml_name}')
    except Exception:
        fatal(f'ERROR: Can\'t open {yaml_name}')


if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger(__name__)

    shortopts = 'ho:v:'
    longopts = [
        'license_key=',
        'license-key=',
        'show_license=',
        'show-license=',
        'help',
        'python_pipeline_config=',
        'python-pipeline-config=',
        'python_pipeline_path=',
        'python-pipeline-path=',
        'python_path=',
        'python-path=',
        'query_string=',
        'query-string=',
        'loop_count=',
        'loop-count=',
        'verbose_manifest=',
        'verbose_manifest=',
    ]
    try:
        logger.debug(f'calling getopt on {sys.argv[1:]}')
        opts, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
    except getopt.GetoptError as err:
        print(f'{sys.argv[0]}: {err}', file=sys.stderr)
        usage()

    license_key = os.getenv('UspLicenseKey')
    show_license = False
    python_pipeline_path = None
    python_pipeline_config = None
    python_path = get_default_python_path()
    query_string = os.environ.get('QUERY_STRING')
    loop_count = 1
    output_file = 'stdout:'
    verbose = 3  # INFO
    verbose_manifest = True

    for opt, arg in opts:
        opt = opt.replace('_', '-')
        if opt == '--license-key':
            license_key = arg
        elif opt == '--show-license':
            show_license = True
        elif opt in ('-h', '--help'):
            usage()
        elif opt == '--python-pipeline-path':
            python_pipeline_path = arg
        elif opt == '--python-pipeline-config':
            python_pipeline_config = arg
        elif opt == '--python-path':
            python_path = arg
        elif opt == '--query-string':
            query_string = arg
        elif opt == '--loop-count':
            loop_count = arg
        elif opt == '-o':
            output_file = arg
        elif opt == '-v':
            try:
                verbose = int(arg)
            except ValueError:
                verbose = -1
            # 0=ERROR 1=WARNING 2=NOTICE 3=INFO 4=DEBUG 5=TRACE
            if verbose < 0 or verbose > 5:
                print(f'ERROR: invalid verbose level "{arg}"', file=sys.stderr)
                usage()
        elif opt == '--verbose-manifest':
            if arg.lower() in ['true', '1', 'y', 'yes']:
                verbose_manifest = True
            else:
                verbose_manifest = False
        else:
            usage()

    if verbose >= 3:
        print(f'manifest_edit {libfmp4.version_string()}    Copyright 2007-2025 CodeShop B.V.\n', file=sys.stderr)

    if verbose == 0:
        logging.getLogger().setLevel(logging.ERROR)
    elif verbose == 1:
        logging.getLogger().setLevel(logging.WARNING)
    elif verbose == 2 or verbose == 3:
        logging.getLogger().setLevel(logging.INFO)
    elif verbose >= 4:
        logging.getLogger().setLevel(logging.DEBUG)

    logger.debug(module_report(libfmp4))
    logger.debug(module_report(yaml))
    logger.debug(module_report(schema))

    logger.debug(f'license_key={license_key}')
    logger.debug(f'show_license={show_license}')
    logger.debug(f'python_pipeline_path={python_pipeline_path}')
    logger.debug(f'python_pipeline_config={python_pipeline_config}')
    logger.debug(f'python_path={python_path}')
    logger.debug(f'loop_count={loop_count}')
    logger.debug(f'output_file={output_file}')
    logger.debug(f'verbose={verbose}')
    logger.debug(f'args={args}')
    logger.debug(f'verbose_manifest={verbose_manifest}')

    if len(args) == 0:
        print('ERROR: no input filename provided!', file=sys.stderr)
        usage()

    if len(args) > 1:
        print('ERROR: too many input filenames provided!', file=sys.stderr)
        usage()

    # Always parse query parameters. These will be passed to the processing
    # pipeline. Some use case may require access to them (e.g. vbegin/vend).
    query_dict = parse_qs(query_string)  # empty dict if query string empty

    if not python_pipeline_config and query_dict:
        # If no python_pipeline_config was specified on the command line, look in the environment
        # for QUERY_STRING. If this is set, and has a query argument python_pipeline_config=basename
        # use that as the basename.
        if 'python_pipeline_config' in query_dict:
            query_arg = query_dict['python_pipeline_config'][0]
            if query_arg:
                # check the argument's sanity. It must have one or more parts separated by
                # slashes, each part consisting only of numbers, letters, dashes or underscores.
                match = re.fullmatch(r'([0-9A-Z_a-z-]+)(/([0-9A-Z_a-z-]+))*', query_arg)
                if not match:
                    fatal(f'ERROR: invalid python_pipeline_config query argument: {query_arg}')
                python_pipeline_config = match.group(0) + '.yaml'

    if not python_pipeline_config:
        print('ERROR: no valid python pipeline configuration file provided!', file=sys.stderr)
        usage()

    if python_pipeline_path:
        python_pipeline_config = os.path.join(python_pipeline_path, python_pipeline_config)

    logger.debug(f'get format from {python_pipeline_config}')
    format = get_format_from_yaml(python_pipeline_config)
    input_file = args[0]

    if True:
        logger.debug(f'format={format}')
        logger.debug(f'input_file={input_file}')

    if python_path is not None:
        python_path_parts = python_path.split(os.pathsep)
        logger.debug(f'inserting {python_path_parts} into sys.path')
        sys.path[1:1] = python_path_parts
    logger.debug(f'sys.path={sys.path}')

    logger.debug('loading manifest_edit entrypoints')
    import manifest_edit.entrypoints as ep
    logger.debug(f'setting up entrypoints with {python_pipeline_config}')
    ep.setup(python_pipeline_config)

    logger.debug('creating libfmp4 context')
    context = libfmp4.Context()

    if format == 'mpd':
        logger.debug(f'loading mpd manifest {input_file}')
        fmp4_manifest = libfmp4.mpd.load_manifest(context, input_file)
    elif format == 'm3u8_main':
        logger.debug(f'loading hls master playlist {input_file}')
        fmp4_manifest = libfmp4.hls.load_master_playlist(context, input_file)
    else:
        logger.debug(f'loading hls media playlist {input_file}')
        fmp4_manifest = libfmp4.hls.load_media_playlist(context, input_file)

    logger.debug(f'updating fmp4_manifest={fmp4_manifest}')
    new_manifest = ep.update_manifest(
        context,
        fmp4_manifest,
        storage=query_dict,
        verbose_manifest=verbose_manifest
        )

    logger.debug(f'updated new_manifest={new_manifest}')

    if format == 'mpd':
        logger.debug(f'saving mpd manifest {output_file}')
        libfmp4.mpd.save_manifest(context, new_manifest, output_file)
    elif format == 'm3u8_main':
        logger.debug(f'saving hls master playlist {output_file}')
        libfmp4.hls.save_master_playlist(context, new_manifest, output_file)
    else:
        logger.debug(f'saving hls media playlist {output_file}')
        libfmp4.hls.save_media_playlist(context, new_manifest, output_file)

    logger.debug('done')
