import abc
import os
import posixpath
import sys
from typing import List, Optional, Tuple, Type

import colorama
from annet.annlib.output import (  # pylint: disable=unused-import
    LABEL_NEW_PREFIX,
    OutputWriter,
    TextArgs,
    capture_output,
    dir_or_file_output,
    format_file_diff,
    print_as_json,
    print_as_yaml,
    print_err_label,
    print_label,
)
from contextlog import get_logger

from annet.cli_args import FileOutOptions, QueryOptions
from annet.connectors import Connector
from annet.storage import Device, storage_connector


class _DriverConnector(Connector["OutputDriver"]):
    name = "OutputDriver"
    ep_name = "output"

    def _get_default(self) -> Type["OutputDriver"]:
        return OutputDriverBasic


output_driver_connector = _DriverConnector()


class OutputDriver(abc.ABC):
    @abc.abstractmethod
    def write_output(self, arg_out: FileOutOptions, items, query_result_count=1) -> None:
        pass

    @abc.abstractmethod
    def format_fails(self, fail, args: Optional[QueryOptions] = None) -> Tuple[str, str]:
        pass

    @abc.abstractmethod
    def cfg_file_names(self, device: Device) -> List[str]:
        pass

    @abc.abstractmethod
    def entire_config_dest_path(self, device: Device, config_path: str) -> str:
        pass


class OutputDriverBasic(OutputDriver):
    def write_output(self, arg_out: FileOutOptions, items, query_result_count=1):
        """
        пишет результаты генерации в файл или директорию :dest
        :dest - это директория в случаях:
        - заканчивается на "/"
        - существует директория с таким именем
        - более одного устройства в результате запроса
        - есть entire-генераторы (определяется по типу первого результата, если устройство одно)
        """
        logger = get_logger()

        items_iter = iter(items)
        try:
            first_result = next(items_iter)
        except StopIteration:
            # нет результатов, ничего не пишем и не создаём
            return

        def _reassemble_items():
            yield first_result
            yield from items_iter

        dest = arg_out.dest
        dir_mode = dir_or_file_output(dest, query_result_count, suggest_dir=(os.sep in first_result[0]))
        _reassembled_items = list(_reassemble_items())

        for output_no, (label, output, is_fail) in enumerate(_reassembled_items):
            writer = output if isinstance(output, OutputWriter) else OutputWriter(output)
            label = os.path.normpath(label)
            label_color = colorama.Back.RED if is_fail else colorama.Back.GREEN
            if dest is None:
                if hasattr(arg_out, "format") and arg_out.format == "json":
                    if output_no > 0:
                        sys.stdout.write(",")
                    elif output_no == 0:
                        sys.stdout.write("{")
                    sys.stdout.write('"%s": ' % label)
                    writer.write(sys.stdout)
                    if output_no == len(_reassembled_items) - 1:
                        sys.stdout.write("}")
                else:
                    if not arg_out.no_label:
                        print_label(label, back_color=label_color)
                    writer.write(sys.stdout)
            elif dir_mode:
                if label.startswith(LABEL_NEW_PREFIX):
                    label = label[len(LABEL_NEW_PREFIX):]
                if label.startswith(os.sep):
                    # just in case.
                    label = label.lstrip(os.sep)
                if os.sep not in label:
                    # vendor config
                    label = os.path.basename(label)
                else:
                    # entire generated file
                    parts = label.split(os.sep)
                    hostname = parts[0]
                    label = os.sep.join(parts[1:])
                    if not arg_out.expand_path:
                        label = os.path.basename(label)
                    if query_result_count > 1:
                        label = os.path.join(hostname, label)
                file_dest = os.path.join(dest, "errors") if is_fail else dest
                out_file = os.path.normpath(os.path.join(file_dest, label))
                logger.info("writing '%s'", out_file)
                dirname = os.path.dirname(out_file)
                if not os.path.exists(dirname):
                    os.makedirs(dirname)
                with open(out_file, "w") as file:
                    writer.write(file)
            else:
                logger.info("writing '%s'", dest)
                with open(dest, "w") as file:
                    writer.write(file)

    def format_fails(self, fail, args: Optional[QueryOptions] = None):
        ret = []
        fqdns = {}
        if args:
            connector = storage_connector.get()
            storage_opts = connector.opts().from_cli_opts(args)
            with connector.storage()(storage_opts) as storage:
                fqdns = storage.resolve_fdnds_by_query(args.query)
        for (assignment, exc) in fail.items():
            label = assignment
            if assignment in fqdns:
                label = fqdns[assignment]
            elif isinstance(assignment, tuple):
                label = assignment[0]
            else:
                ValueError("Failed to parse failed assignment %r" % assignment)
            ret.append((label, getattr(exc, "formatted_output", f"{repr(exc)} (formatted_output is absent)"), True))
        return ret

    def cfg_file_names(self, device: Device) -> List[str]:
        return [f"{device.hostname}.cfg"]

    def entire_config_dest_path(self, device, config_path: str) -> str:
        """Формирует путь к конфигу в директории destname.

        Например, для устройства с hostname `my-device`:
        ```
        >>> device.entire_config_dest_path("/etc/frr/frr.conf")
        'my-device.cfg/etc/frr/frr.conf'
        >>>
        ```
        """
        # NOTE: с полученным `config_path` работаем через `posixpath`, а не через `os.path`, потому что
        #       entire-путь POSIX-специфичный; но в конце формируем путь через `os.path` для текущей платформы
        if not posixpath.abspath(config_path):
            raise RuntimeError(f"Want absolute config path, but relative received: {config_path}")
        cfg_files = self.cfg_file_names(device)
        # NOTE: получаем путь без "/" в начале, например, "etc/frr/frr.conf"
        relative_config_path = posixpath.relpath(config_path, "/")
        dest_config_path_parts = [cfg_files[0]] + relative_config_path.split(posixpath.sep)
        return os.path.join(*dest_config_path_parts)
