from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Type
from typing import Union
from typing import cast

from pytezos.context.abstract import AbstractContext
from pytezos.michelson.micheline import Micheline
from pytezos.michelson.micheline import MichelsonRuntimeError
from pytezos.michelson.types import OrType
from pytezos.michelson.types.adt import wrap_or
from pytezos.michelson.types.adt import wrap_parameters
from pytezos.michelson.types.base import MichelsonType
from pytezos.michelson.types.base import parse_name
from pytezos.michelson.types.core import Unit


class ParameterSection(Micheline, prim='parameter', args_len=1):
    args: List[Type[MichelsonType]]  # type: ignore
    root_name: str

    def __init__(self, item: MichelsonType):
        super().__init__()
        self.item = item

    def __repr__(self):
        return repr(self.item)

    @staticmethod
    def match(type_expr) -> Type['ParameterSection']:
        try:
            cls = Micheline.match(type_expr)
            if not issubclass(cls, ParameterSection):
                cls = ParameterSection.create_type(args=[cls])
        except Exception as e:
            raise MichelsonRuntimeError('parameter', *e.args) from e
        return cls

    @classmethod
    def create_type(
        cls,
        args: List[Union[Type['Micheline'], Any]],
        annots: Optional[list] = None,
        **kwargs,
    ) -> Type['ParameterSection']:
        assert not annots, 'top level parameter annotations not supported'

        root_type = cast(Type[MichelsonType], args[0])
        if issubclass(root_type, OrType):
            root_name = root_type.field_name  # type: ignore
            if not root_name:
                flat_args = root_type.get_flat_args(entrypoints=True)  # type: ignore
                assert isinstance(flat_args, dict), f'expected a named type, got {flat_args}'
                root_name = 'root' if 'default' in flat_args else 'default'
        else:
            root_name = root_type.field_name or 'default'
        res = type(cls.__name__, (cls,), dict(args=args, root_name=root_name, **kwargs))
        return cast(Type['ParameterSection'], res)

    @classmethod
    def execute(cls, stack, stdout: List[str], context: AbstractContext):
        context.set_parameter_expr(cls.as_micheline_expr())
        stdout.append(f'parameter: updated')

    @classmethod
    def list_entrypoints(cls) -> Dict[str, Type[MichelsonType]]:
        entrypoints = {}
        root_type = cls.args[0]
        if issubclass(root_type, OrType):
            flat_args = root_type.get_flat_args(entrypoints=True)
            assert isinstance(flat_args, dict), f'expected dict of named entrypoints'
            for name, arg in flat_args.items():
                entrypoints[name] = arg.get_anon_type()
        entrypoints[cls.root_name] = root_type
        return entrypoints

    @classmethod
    def from_parameters(cls, parameters: Dict[str, Any]) -> 'ParameterSection':
        if len(parameters) == 0:
            parameters = {'entrypoint': 'default', 'value': {'prim': 'Unit'}}
        assert isinstance(parameters, dict) and parameters.keys() == {
            'entrypoint',
            'value',
        }, f'expected {{entrypoint, value}}, got {parameters}'
        entrypoint = parameters['entrypoint']
        if entrypoint == cls.root_name:
            res = cls.from_micheline_value(parameters['value'])
            return cast(ParameterSection, res)
        else:
            root_type = cls.args[0]
            assert issubclass(root_type, OrType), f'expected `{cls.root_name}`, got `{entrypoint}`'
            _, key_to_path, _ = root_type.get_type_layout(entrypoints=True)
            assert entrypoint in key_to_path, f'unexpected entrypoint `{entrypoint}`'  # type: ignore
            val_expr = wrap_parameters(parameters['value'], key_to_path[entrypoint])  # type: ignore
            item = root_type.from_micheline_value(val_expr)
            return cls(item)

    def to_parameters(self, mode='readable') -> Dict[str, Any]:
        entrypoint, item = self.root_name, self.item
        if isinstance(self.item, OrType):
            flat_values = self.item.get_flat_values(entrypoints=True)
            assert isinstance(flat_values, dict) and len(flat_values) == 1, f'expected named type'
            entrypoint, item = next(iter(flat_values.items()))
        return {
            'entrypoint': entrypoint,
            'value': item.to_micheline_value(mode=mode, lazy_diff=None),
        }

    @classmethod
    def generate_pydoc(cls) -> str:
        definitions = []  # type: ignore
        res = cls.args[0].generate_pydoc(definitions, cls.prim)
        if res != f'${cls.prim}':
            definitions.insert(0, (cls.prim, res))
        return '\n'.join(f'${var}:\n\t{doc}\n' for var, doc in definitions)

    @classmethod
    def from_micheline_value(cls, val_expr) -> 'ParameterSection':
        item = cls.args[0].from_micheline_value(val_expr)
        return cls(item)

    @classmethod
    def from_python_object(cls, py_obj) -> 'ParameterSection':
        if isinstance(py_obj, str):
            entrypoint = py_obj
            py_obj = {entrypoint: Unit}
        else:
            if not isinstance(py_obj, dict) or len(py_obj) != 1:
                raise TypeError(f'expected dict with a single key, got {type(py_obj).__name__} `{py_obj}`')
            entrypoint = next(iter(py_obj))

        if entrypoint == cls.root_name:
            item = cls.args[0].from_python_object(py_obj[entrypoint])
        else:
            if not issubclass(cls.args[0], OrType):
                raise TypeError(f'Unexpected entrypoint `{entrypoint}`: parameter is not of sum type')
            _, key_to_path, _ = cls.args[0].get_type_layout(infer_names=True, entrypoints=True)
            if not key_to_path:
                raise TypeError('sum type has to be named (in the scope of PyTezos)')
            item = cls.args[0].from_python_object(wrap_or(py_obj[entrypoint], key_to_path[entrypoint]))

        return cls(item)

    def to_micheline_value(self, mode='readable', lazy_diff=None):
        return self.item.to_micheline_value(mode=mode, lazy_diff=None)

    def to_python_object(self, try_unpack=False, lazy_diff=None) -> dict:
        py_obj = self.item.to_python_object(
            try_unpack=try_unpack,
            lazy_diff=None,
        )
        if issubclass(self.args[0], OrType):
            return py_obj
        else:
            return {self.root_name: py_obj}

    def merge_lazy_diff(self, lazy_diff: List[dict]) -> 'ParameterSection':
        item = self.item.merge_lazy_diff(lazy_diff)
        return type(self)(item)

    def attach_context(self, context: AbstractContext):
        self.item.attach_context(context, big_map_copy=True)

    def aggregate_lazy_diff(self, mode='readable') -> List[dict]:
        lazy_diff = []  # type: ignore
        self.item.aggregate_lazy_diff(lazy_diff, mode=mode)
        return lazy_diff

    def __getitem__(self, key):
        assert hasattr(self.item, '__getitem__'), f'index access is not implemented for {self.item.prim}'
        return self.item[key]
