#
# This file is part of the Ingram Micro CloudBlue Connect EaaS Extension Runner.
#
# Copyright (c) 2022 Ingram Micro. All Rights Reserved.
#
import logging
import os
import subprocess
from datetime import (
    datetime,
)
from importlib.metadata import (
    entry_points,
    version,
)
from uuid import (
    uuid4,
)

import requests
from rich import (
    box,
)
from rich.align import (
    Align,
)
from rich.markdown import (
    Markdown,
)
from rich.syntax import (
    Syntax,
)
from rich.table import (
    Table,
)

from connect.client import (
    ClientError,
    ConnectClient,
)
from connect.eaas.core.validation.models import (
    ValidationItem,
    ValidationResult,
)
from connect.eaas.core.validation.validators import (
    get_validators,
)
from connect.eaas.runner.constants import (
    BACKGROUND_TASK_MAX_EXECUTION_TIME,
    HANDLER_CLASS_TITLE,
    INTERACTIVE_TASK_MAX_EXECUTION_TIME,
    ORDINAL_SUFFIX,
    PYPI_EXTENSION_RUNNER_URL,
    ROW_TRANSFORMATION_TASK_MAX_EXECUTION_TIME,
    SCHEDULED_TASK_MAX_EXECUTION_TIME,
    TRANSFORMATION_TASK_MAX_EXECUTION_TIME,
    TRANSFORMATION_WRITE_QUEUE_TIMEOUT,
)


logger = logging.getLogger('connect.eaas')


def get_container_id():
    result = subprocess.run(
        ['cat', '/proc/1/cpuset'],
        capture_output=True,
        stdin=subprocess.DEVNULL,
        start_new_session=True,
    )
    try:
        result.check_returncode()
    except subprocess.CalledProcessError:
        return str(uuid4())

    _, container_id = result.stdout.decode()[:-1].rsplit('/', 1)
    if len(container_id) == 64:
        return container_id

    result = subprocess.run(
        ['grep', 'overlay', '/proc/self/mountinfo'],
        capture_output=True,
        stdin=subprocess.DEVNULL,
        start_new_session=True,
    )
    try:
        result.check_returncode()
        mount = result.stdout.decode()
        start_idx = mount.index('upperdir=') + len('upperdir=')
        end_idx = mount.index(',', start_idx)
        dir_path = mount[start_idx:end_idx]
        _, container_id, _ = dir_path.rsplit('/', 2)
        if len(container_id) != 64:
            return str(uuid4())
        return container_id
    except (subprocess.CalledProcessError, ValueError):
        return str(uuid4())


def get_environment():
    return {
        'api_key': os.getenv('API_KEY'),
        'environment_id': os.getenv('ENVIRONMENT_ID'),
        'instance_id': os.getenv('INSTANCE_ID', get_container_id()),
        'ws_address': os.getenv('SERVER_ADDRESS', 'api.cnct.info'),
        'api_address': os.getenv('API_ADDRESS', os.getenv('SERVER_ADDRESS', 'api.cnct.info')),
        'background_task_max_execution_time': int(os.getenv(
            'BACKGROUND_TASK_MAX_EXECUTION_TIME', BACKGROUND_TASK_MAX_EXECUTION_TIME,
        )),
        'interactive_task_max_execution_time': int(os.getenv(
            'INTERACTIVE_TASK_MAX_EXECUTION_TIME', INTERACTIVE_TASK_MAX_EXECUTION_TIME,
        )),
        'scheduled_task_max_execution_time': int(os.getenv(
            'SCHEDULED_TASK_MAX_EXECUTION_TIME', SCHEDULED_TASK_MAX_EXECUTION_TIME,
        )),
        'transformation_task_max_execution_time': int(os.getenv(
            'TRANSFORMATION_TASK_MAX_EXECUTION_TIME', TRANSFORMATION_TASK_MAX_EXECUTION_TIME,
        )),
        'row_transformation_task_max_execution_time': int(
            os.getenv(
                'ROW_TRANSFORMATION_TASK_MAX_EXECUTION_TIME',
                ROW_TRANSFORMATION_TASK_MAX_EXECUTION_TIME,
            ),
        ),
        'transformation_write_queue_timeout': int(os.getenv(
            'TRANSFORMATION_WRITE_QUEUE_TIMEOUT', TRANSFORMATION_WRITE_QUEUE_TIMEOUT,
        )),
    }


def get_version():
    try:
        return version('connect-extension-runner')
    except Exception:
        return '0.0.0'


def to_ordinal(val):
    if val > 14:
        return f"{val}{ORDINAL_SUFFIX.get(int(str(val)[-1]), 'th')}"
    return f"{val}{ORDINAL_SUFFIX.get(val, 'th')}"


def get_connect_version():
    connect_client = get_client()
    try:
        connect_client.accounts.all().first()
    except ClientError as ce:
        if ce.status_code in [401, 403]:
            raise Exception(
                'Cannot check the current EaaS Runner version: '
                f'API key is not valid: {ce}.')
        raise Exception(f'Cannot check the current EaaS Runner version: {ce}.')

    return connect_client.response.headers['Connect-Version']


def get_pypi_runner_latest_version(major_version):
    res = requests.get(PYPI_EXTENSION_RUNNER_URL)
    if res.status_code != 200:
        logger.error(
            f'Cannot check the current EaaS Runner version: {res.text}.',
        )
        raise Exception(f'Cannot check the current EaaS Runner version: {res.text}.')

    content = res.json()
    tags = [
        int(version.split('.')[1])
        for version in content['releases'] if version.startswith(f'{major_version}.')
    ]
    if tags:
        return f'{major_version}.{str(max(tags))}'
    return content['info']['version']


def get_client():
    env = get_environment()
    api_key = env['api_key']
    api_address = env['api_address']
    return ConnectClient(
        api_key,
        endpoint=f'https://{api_address}/public/v1',
        use_specs=False,
        max_retries=3,
    )


def get_current_environment():
    env = get_environment()
    environment_id = env['environment_id']
    if environment_id.startswith('ENV-'):
        service_id = f'SRVC-{environment_id[4:-3]}'
        client = get_client()
        try:
            return (
                client('devops').services[service_id].environments[environment_id].get()
            )
        except ClientError as ce:
            logger.warning(f'Cannot retrieve environment information: {ce}')


def notify_process_restarted(process_type):
    env = get_environment()
    current_environment = get_current_environment()
    if current_environment and current_environment['runtime'] == 'cloud':
        instance_id = env['instance_id']
        environment_id = current_environment['id']
        service_id = f'SRVC-{environment_id[4:-3]}'
        client = get_client()
        try:
            client('devops').services[service_id].environments[environment_id].update(
                {
                    'error_output': (
                        f'Process {process_type} worker of instance '
                        f'{instance_id} has been '
                        f'restarted at {datetime.now().isoformat()}'
                    ),
                },
            )
        except ClientError as ce:
            logger.warning(f'Cannot notify {process_type} process restart: {ce}')


def iter_entry_points(group, name=None):
    group_entrypoints = entry_points().get(group)
    if not group_entrypoints:
        return
    for ep in group_entrypoints:
        if name:
            if ep.name == name:
                yield ep
        else:
            yield ep


def configure_logger(debug, no_rich):
    logging.config.dictConfig(
        {
            'version': 1,
            'disable_existing_loggers': False,
            'formatters': {
                'verbose': {
                    'format': '%(asctime)s %(name)s %(levelname)s PID_%(process)d %(message)s',
                },
                'rich': {
                    'format': '%(message)s',
                },
            },
            'filters': {},
            'handlers': {
                'console': {
                    'class': 'logging.StreamHandler',
                    'formatter': 'verbose',
                },
                'rich': {
                    'class': 'rich.logging.RichHandler',
                    'formatter': 'rich',
                    'log_time_format': lambda x: x.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3],
                    'rich_tracebacks': True,

                },
            },
            'loggers': {
                'connect.eaas': {
                    'handlers': ['console'] if no_rich else ['rich'],
                    'level': 'DEBUG' if debug else 'INFO',
                },
                'eaas': {
                    'handlers': ['console'] if no_rich else ['rich'],
                    'level': 'DEBUG' if debug else 'INFO',
                },
            },
        },
    )


def check_runner_version(context):
    try:
        connect_full_version = get_connect_version()
        connect_version = connect_full_version.split('.')[0]
        runner_version = get_version()
        latest_runner_version = get_pypi_runner_latest_version(connect_version)

        if runner_version != latest_runner_version:
            return ValidationResult(
                items=[
                    ValidationItem(
                        level='ERROR',
                        message=(
                            'Runner is outdated, please, update. '
                            f'Required version {latest_runner_version}, '
                            f'current version: {runner_version}.'
                        ),
                    ),
                ],
                must_exit=True,
            )
    except Exception as e:
        return ValidationResult(
            items=[
                ValidationItem(level='ERROR', message=str(e)),
            ],
            must_exit=True,
        )
    return ValidationResult()


def validate_extension():
    validation_items = []
    event_definitions = {
        definition['type']: definition
        for definition in get_client()('devops').event_definitions.all()
    }
    context = {
        'runner_version': get_version(),
        'project_dir': os.getcwd(),
        'event_definitions': event_definitions,
    }
    for validator in [check_runner_version, *get_validators()]:
        result = validator(context=context)
        validation_items.extend(result.items)
        if result.must_exit:
            break
        if result.context:
            context.update(result.context)

    higest_message_level = 'INFO'

    tables = []

    if validation_items:
        higest_message_level = 'WARNING'
        for item in validation_items:
            if item.level == 'ERROR':
                higest_message_level = 'ERROR'
            table = Table(
                box=box.ROUNDED,
                show_header=False,
            )
            table.add_column('Field', style='blue')
            table.add_column('Value', overflow='fold')
            level_color = 'red' if item.level == 'ERROR' else 'yellow'
            table.add_row('Level', f'[bold {level_color}]{item.level}[/]')
            table.add_row('Message', Markdown(item.message))
            table.add_row('File', item.file or '-')
            table.add_row(
                'Code',
                Syntax(
                    item.code,
                    'python3',
                    theme='ansi_light',
                    dedent=True,
                    line_numbers=True,
                    start_line=item.start_line,
                    highlight_lines={item.lineno},
                ) if item.code else '-',
            )
            tables.append(table)
    return higest_message_level, tables


def get_no_features_table():
    table = Table(
        box=box.ROUNDED,
        show_header=False,
    )
    table.add_column('Field', style='blue')
    table.add_column('Value')
    table.add_row('Level', '[bold red]ERROR[/]')
    table.add_row(
        'Message',
        (
            'No feature available for current extension. '
            'Did you run poetry install ?'
        ),
    )
    table.add_row('File', '-')
    table.add_row('Code', '-')
    return table


def get_anvilapp_detail_table(details):
    callables_table = None

    callables = details['features'].get('callables')

    if callables:
        callables_table = Table(
            box=box.MINIMAL_HEAVY_HEAD,
            title='Anvil Callables',
            title_style='blue',
            show_header=True,
            expand=True,
            row_styles=['', 'dim'],
        )
        callables_table.add_column('Method')
        callables_table.add_column('Summary')
        callables_table.add_column('Description')
        for callable in callables:
            callables_table.add_row(
                callable['method'],
                callable['summary'],
                callable['description'],
            )

    return callables_table


def get_eventsapp_detail_table(details):
    events_table = None
    schedulables_table = None
    grid = None

    events = details['features'].get('events')
    schedulables = details['features'].get('schedulables')

    if events:
        events_table = Table(
            box=box.MINIMAL_HEAVY_HEAD,
            title='Event Subscriptions',
            title_style='blue',
            show_header=True,
            expand=True,
            row_styles=['', 'dim'],
        )
        events_table.add_column('Method Name')
        events_table.add_column('Event Type')
        events_table.add_column('Statuses')
        for event in events.values():
            events_table.add_row(
                event['method'],
                event['event_type'],
                ', '.join(event['statuses'] or []),
            )

    if schedulables:
        schedulables_table = Table(
            box=box.MINIMAL_HEAVY_HEAD,
            title='Schedulable Methods',
            title_style='blue',
            show_header=True,
            expand=True,
            row_styles=['', 'dim'],
        )
        schedulables_table.add_column('Method Name')
        schedulables_table.add_column('Name')
        schedulables_table.add_column('Description')
        for schedulable in schedulables:
            schedulables_table.add_row(
                schedulable['method'],
                schedulable['name'],
                schedulable['description'],
            )

    if events_table and schedulables_table:
        grid = Table.grid(expand=True)
        grid.add_column()
        grid.add_row(events_table)
        grid.add_row(schedulables_table)

    return grid or events_table or schedulables_table


def get_webapp_detail_table(details):
    auth_table = None
    no_auth_table = None
    ui_modules_table = None
    grid = None

    auth_endpoints = details['features']['endpoints'].get('auth')
    no_auth_endpoints = details['features']['endpoints'].get('no_auth')
    ui_modules = details['features'].get('ui_modules')

    if auth_endpoints:
        auth_table = Table(
            box=box.MINIMAL_HEAVY_HEAD,
            title='Authenticated API Endpoints',
            title_style='blue',
            show_header=True,
            expand=True,
            row_styles=['', 'dim'],
        )
        auth_table.add_column('Summary')
        auth_table.add_column('Method')
        auth_table.add_column('Path')
        for endpoint in auth_endpoints:
            auth_table.add_row(
                endpoint['summary'],
                endpoint['method'],
                endpoint['path'],
            )

    if no_auth_endpoints:
        no_auth_table = Table(
            box=box.MINIMAL_HEAVY_HEAD,
            title='Non Authenticated API Endpoints',
            title_style='blue',
            show_header=True,
            expand=True,
            row_styles=['', 'dim'],
        )
        no_auth_table.add_column('Summary')
        no_auth_table.add_column('Method')
        no_auth_table.add_column('Path')
        for endpoint in no_auth_endpoints:
            no_auth_table.add_row(
                endpoint['summary'],
                endpoint['method'],
                endpoint['path'],
            )

    if ui_modules:
        ui_modules_table = Table(
            box=box.MINIMAL_HEAVY_HEAD,
            title='UI Components',
            title_style='blue',
            show_header=True,
            expand=True,
            row_styles=['', 'dim'],
        )
        ui_modules_table.add_column('Name')
        ui_modules_table.add_column('Url')
        ui_modules_table.add_column('Integration point')
        for ui_module in ui_modules:
            ui_modules_table.add_row(
                ui_module['name'],
                ui_module['url'],
                ui_module['integration_point'],
            )

    grid = Table.grid(expand=True)
    grid.add_column()
    if auth_table:
        grid.add_row(auth_table)
    if no_auth_table:
        grid.add_row(no_auth_table)
    if ui_modules_table:
        grid.add_row(ui_modules_table)

    return grid


def get_tfnapp_detail_table(details):
    grid = Table.grid(expand=True)
    grid.add_column()

    transformations = details['features'].get('transformations')

    if transformations:
        table = Table(
            box=box.MINIMAL_HEAVY_HEAD,
            title='Transformations',
            title_style='blue',
            show_header=True,
            expand=True,
            row_styles=['', 'dim'],
        )
        table.add_column('Name')
        table.add_column('Description')
        table.add_column('Edit dialog')
        table.add_column('Method')
        for transformation in transformations:
            table.add_row(
                transformation['name'],
                transformation['description'],
                transformation['edit_dialog_ui'],
                transformation['method'],
            )

        grid.add_row(table)

    return grid


def get_features_table(features):
    table = Table(
        box=box.ROUNDED,
        title='Available Features',
        title_style='blue',
        show_header=True,
        show_lines=True,
        expand=True,
    )
    table.add_column('Feature Name')
    table.add_column('Supported', width=9)
    table.add_column('Details')

    for feature_handler, details in features.items():
        if not details['available']:
            table.add_row(
                HANDLER_CLASS_TITLE[feature_handler],
                Align.center('[red]\u2716[/]'),
                '-',
            )
            continue
        if feature_handler == 'EventsApp':
            table.add_row(
                HANDLER_CLASS_TITLE[feature_handler],
                Align.center('[green]\u2714[/]'),
                Align.center(get_eventsapp_detail_table(details)),
            )
        elif feature_handler == 'WebApp':
            table.add_row(
                HANDLER_CLASS_TITLE[feature_handler],
                Align.center('[green]\u2714[/]'),
                Align.center(get_webapp_detail_table(details)),
            )
        elif feature_handler == 'AnvilApp':
            table.add_row(
                HANDLER_CLASS_TITLE[feature_handler],
                Align.center('[green]\u2714[/]'),
                Align.center(get_anvilapp_detail_table(details)),
            )
        elif feature_handler == 'TfnApp':
            table.add_row(
                HANDLER_CLASS_TITLE[feature_handler],
                Align.center('[green]\u2714[/]'),
                Align.center(get_tfnapp_detail_table(details)),
            )
    return table
