""" Command Line Interface for W24 Techread
"""
import argparse
import io
import json
import logging
from collections import namedtuple
from typing import Any, Dict, List, Optional

from dotenv import load_dotenv
from pydantic import UUID4, ValidationError

from werk24.cli import utils
from werk24.exceptions import RequestTooLargeException
from werk24.models.ask import (
    W24AskCanvasThumbnail,
    W24AskPageThumbnail,
    W24AskSectionalThumbnail,
    W24AskSheetAnonymization,
    W24AskSheetThumbnail,
    W24AskTitleBlock,
    W24AskVariantCAD,
    W24AskVariantExternalDimensions,
    W24AskVariantGDTs,
    W24AskVariantMeasures,
    W24AskVariantRadii,
    W24AskVariantRoughnesses,
    W24AskVariantThreadElements,
)
from werk24.models.techread import (
    W24TechreadException,
    W24TechreadMessageSubtypeError,
    W24TechreadMessageSubtypeProgress,
    W24TechreadMessageType,
)
from werk24.techread_client import Hook

# load the environment variables
load_dotenv(".werk24")

# set the log level to info for the test setting
# We recommend using logging.WARNING for production
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s.%(msecs)03d %(levelname)s %(module)s - %(funcName)s: %(message)s',  # noqa
    datefmt='%Y-%m-%d %H:%M:%S')

# get the local logger
logger = logging.getLogger(__name__)  # pylint:disable = invalid-name


# make the configuration of what hooks we want to handle and how
HookConfig = namedtuple("HookConfig", "arg ask function")
hook_config = [
    HookConfig(
        'ask_page_thumbnail',
        W24AskPageThumbnail,
        lambda m: _show_image("Page Thumbnail", m.payload_bytes)),
    HookConfig(
        'ask_sheet_thumbnail',
        W24AskSheetThumbnail,
        lambda m: _show_image("Sheet Thumbnail", m.payload_bytes)),
    HookConfig(
        'ask_sheet_anonymization',
        W24AskSheetAnonymization,
        lambda m: _show_image("Sheet Anonymization", m.payload_bytes)
    ),
    HookConfig(
        'ask_canvas_thumbnail',
        W24AskCanvasThumbnail,
        lambda m: _show_image("Canvas Thumbnail", m.payload_bytes)),
    HookConfig(
        'ask_sectional_thumbnail',
        W24AskSectionalThumbnail,
        lambda m: _show_image("Sectional Thumbnail", m.payload_bytes)),
    # HookConfig(
    #     'ask_variant_angles',
    #     W24AskVariantAngles,
    #     lambda m: _print_payload("Ask Variant Angles", m.payload_dict)),
    HookConfig(
        'ask_variant_external_dimensions',
        W24AskVariantExternalDimensions,
        lambda m: _print_payload("Ask Variant Ext. Dimensions", m.payload_dict)),
    HookConfig(
        'ask_variant_gdts',
        W24AskVariantGDTs,
        lambda m: _print_payload("Ask Variant GDTs", m.payload_dict)),
    HookConfig(
        'ask_variant_measures',
        W24AskVariantMeasures,
        lambda m: _print_payload("Ask Variant Measures", m.payload_dict)),
    HookConfig(
        'ask_variant_radii',
        W24AskVariantRadii,
        lambda m: _print_payload("Ask Variant Radii", m.payload_dict)),
    HookConfig(
        'ask_variant_roughnesses',
        W24AskVariantRoughnesses,
        lambda m: _print_payload("Ask Variant Roughnesses", m.payload_dict)),
    HookConfig(
        'ask_variant_cad',
        W24AskVariantCAD,
        lambda m: _store_variant_cad(
            m.payload_dict,
            m.payload_bytes,
            m.exceptions)),
    HookConfig(
        'ask_titleblock',
        W24AskTitleBlock,
        lambda m: _print_payload("Ask TitleBlock", m.payload_dict)
    ),
    HookConfig(
        'ask_variant_thread_elements',
        W24AskVariantThreadElements,
        lambda m: _print_payload("Ask Variant Thread Elements", m.payload_dict)
    ),
]


def _store_variant_cad(
    payload_dict: Dict[str, Any],
    payload_bytes: Optional[bytes],
    exceptions: List[W24TechreadException]
) -> None:
    """ Store the CAD file the current directory

    Args:
        payload_dict (Dict[str, Any]): Payload Dictionary
        payload_bytes (bytes): CAD that we received as response
    """
    logger.info(f"Ask Variant CAD\n{payload_dict}")

    # print potential exceptions
    if exceptions:
        _log_exceptions(exceptions)

    # store the payload
    if payload_bytes is not None:
        _store_variant_cad_payload(payload_dict, payload_bytes)


def _log_exceptions(
    exceptions: List[W24TechreadException]
) -> None:
    """ Log the encountered exceptions as warnings

    Args:
        exceptions (List[W24TechreadException]): List of
            encountered exceptions. Can be an empty list.
    """
    for cur_exception in exceptions:
        logger.warn(cur_exception)


def _store_variant_cad_payload(
    payload_dict: Dict[str, Any],
    payload_bytes: bytes
) -> None:
    """ Store the CAD file with the knowledge that
    the payload exists

    Args:
        payload_dict (Dict[str, Any]): Payload Dictionary
        payload_bytes (bytes): CAD response
    """
    # make the filename
    variant_id = payload_dict.get('variant_id')
    filename = f"./w24_ask_variant_cad_{variant_id}.dxf"

    # and write the content
    with open(filename, "wb+") as file_handle:
        file_handle.write(payload_bytes)

    # tell the user
    logger.info(f"CAD response stored in {filename}")


def _get_drawing(
    file_path: str
) -> Optional[bytes]:
    """ Get the bytes of the file that shall be
    read.

    Args:
        file_path (str): path to the input file

    Returns:
        bytes: content of the fiel
    """
    try:
        with open(file_path, "rb") as filehandle:
            return filehandle.read()
    except FileNotFoundError:
        print(f"File {file_path} not found")
        return None


def _print_payload(
    log_text: str,
    payload_dict: Dict[str, Any]
) -> None:
    """ Display the payload dictionary in a format that
    is easy for humans to read

    Args:
        log_text (str): Headline info
        payload_dict (Dict[str, Any]): Payload dictionary
    """
    print(log_text)
    payload_json = json.dumps(payload_dict, indent=4)
    print(payload_json)


def _show_image(
        log_text: str,
        image_bytes: bytes
) -> None:
    """ Display a debug image. The function relies on
    the PIL functionality of Image.show(). The
    actual behavior will thus be different across platforms

    Args:
        log_text (str): Log Text that we want to write back
            to the command line interface
        image_bytes (bytes): byte representation of the image
            that is to be displayed
    """

    # Import the PIL at runtime to make sure that
    # you can still use the client without the need
    # to install Pillow
    try:
        from PIL import Image  # pylint: disable=import-outside-toplevel
    except ImportError:
        logger.warning(
            "Viewing image-like responses requires the installation"
            " of the PIL package: pip install pillow. Image skipped.")
        return

    # otherwise print the text
    logger.info(log_text)

    # and show the image
    image = Image.open(io.BytesIO(image_bytes))
    try:
        image.show(title=log_text)
    except BaseException:
        logger.warning(
            "Image cannot be displayed. "
            "Please check your local security settings.")
        return


async def main(
    args: argparse.Namespace
) -> None:
    """
    Run the Techread command with the CLI arguments

    Args:
        args(argparse.Namespace): CLI args generated by
        argparse
    """

    # interpret the sub-account as UUID4 and stop if that
    # does not conform to the correct pattern.
    try:
        if args.sub_account is not None:
            sub_account = UUID4(args.sub_account)
        else:
            sub_account = None

    except ValueError:
        logger.error("Sub-Account ID does not conform to UUID4-pattern")
        return

    # make the hooks from the arguments
    hooks = _make_hooks_from_args(args)

    # get the client instance and handle
    # potential errors
    client = utils.make_client()

    async with client as session:

        # get the drawing
        drawing_bytes = _get_drawing(args.input_file)
        if drawing_bytes is None:
            return

        # and make the request
        try:
            await session.read_drawing_with_hooks(
                drawing_bytes,
                hooks,
                sub_account=sub_account)

        except RequestTooLargeException:
            message = "Request was too large to be processed. " \
                + "Please check the documentation for current limits."
            logger.error(message)


def _make_hooks_from_args(
        args: argparse.Namespace
) -> List[Hook]:

    # add a general hook to deal with internal errors
    hook_error_internal = Hook(
        message_type=W24TechreadMessageType.ERROR,
        message_subtype=W24TechreadMessageSubtypeError.INTERNAL,
        function=lambda msg:
        logger.error("Internal Error %s", msg.payload_dict))

    # add a general hook to deal with PROGRESS messages
    hook_progress_started = Hook(
        message_type=W24TechreadMessageType.PROGRESS,
        message_subtype=W24TechreadMessageSubtypeProgress.STARTED,
        function=lambda msg: logger.info("Techread process started"))

    # tell the api what asks you are interested in,
    # and define what to do when you receive the result
    hooks = [
        Hook(ask=c.ask(), function=c.function)
        for c in hook_config
        if getattr(args, c.arg)
    ] + [
        hook_error_internal,
        hook_progress_started
    ]

    return hooks
