import json
import re
from contextlib import suppress
from os.path import dirname, join
from typing import Any, Dict, List, Optional, Union

import requests  # type: ignore
from attr import dataclass
from jsonschema import validate as jsonschema_validate  # type: ignore

from pytezos.context.impl import ExecutionContext
from pytezos.context.mixin import ContextMixin
from pytezos.contract import converter
from pytezos.contract.view import ContractView


def _to_camelcase(string: str) -> str:
    expression = re.compile("[^A-Za-z]+")
    words = expression.split(string)
    if len(words) == 1:
        string = string[0].lower() + string[1:]
        return string
    return ''.join(w.lower() if i == 0 else w.title() for i, w in enumerate(words))


_metadata_json_replace_table = {
    '"return-type":': '"returnType":',
    '"michelson-storage-view":': '"michelsonStorageView":',
}

with open(join(dirname(__file__), 'metadata-schema.json')) as file:
    metadata_schema = json.load(file)


@dataclass(kw_only=True, frozen=True)
class StaticError:
    error: str
    expansion: str
    languages: Optional[List[str]] = None


@dataclass(kw_only=True, frozen=True)
class DynamicError:
    view: str
    languages: Optional[List[str]] = None


@dataclass(kw_only=True, frozen=True)
class License:
    name: str
    details: Optional[str] = None


@dataclass(kw_only=True, frozen=True)
class RestApiView:
    specificationUri: str
    baseUri: Optional[str] = None
    path: str
    method: Optional[str] = None


@dataclass(kw_only=True, frozen=True)
class MichelsonStorageView:
    parameter: Optional[Dict[str, Any]] = None
    returnType: Dict[str, Any]
    code: List[Any]
    annotations: Optional[List[Dict[str, Any]]] = None
    version: Optional[str] = None


@dataclass(kw_only=True, frozen=True)
class RestApiViewImplementation:
    restApiQuery: RestApiView


@dataclass(kw_only=True, frozen=True)
class MichelsonStorageViewImplementation:
    michelsonStorageView: MichelsonStorageView


@dataclass(kw_only=True, frozen=True)
class View:
    name: str
    description: Optional[str] = None
    implementations: List[Union[MichelsonStorageViewImplementation, RestApiViewImplementation]]
    pure: bool = False


@dataclass(kw_only=True, repr=False)
class ContractMetadata(ContextMixin):
    """TZIP-16 contract metadata"""

    name: Optional[str] = None
    description: Optional[str] = None
    version: Optional[str] = None
    license: Optional[License] = None
    authors: Optional[List[str]] = None
    interfaces: Optional[List[str]] = None
    errors: Optional[List[Union[StaticError, DynamicError]]] = None
    views: Optional[List[View]] = None

    def __attrs_post_init__(self) -> None:
        self.raw: Dict[str, Any] = {}
        self.storage_view_impl: Dict[str, Any] = {}
        if self.views:
            for view in self.views:
                michelson_storage_impl = next(
                    (impl for impl in view.implementations if isinstance(impl, MichelsonStorageViewImplementation)), None
                )
                if michelson_storage_impl is not None:
                    name = _to_camelcase(view.name)
                    self.storage_view_impl[name] = michelson_storage_impl.michelsonStorageView

    def __repr__(self) -> str:
        res = [
            super().__repr__(),
            '\nMetadata',
            f'.name\t\t{self.name or ""}',
            f'.description\t{self.description or ""}',
            f'.license\t{self.license.name if self.license else ""}',
            f'.authors\t{", ".join(self.authors or [])}',
            f'.interfaces\t{", ".join(self.interfaces or [])}',
            f'.errors\t\t<{len(self.errors or [])}>',
            f'.views\t\t<{len(self.views or [])}>',
            '\nStorage views',
            *list(map(lambda x: f'.{x}()', self.storage_view_impl)),
        ]
        return '\n'.join(res)

    def __getattribute__(self, name: str) -> ContractView:
        with suppress(AttributeError):
            return super().__getattribute__(name)
        try:
            impl = self.storage_view_impl[name]
            return ContractView(
                context=self.context,
                name=name,
                parameter=impl.parameter,
                return_type=impl.returnType,
                code=impl.code,
            )
        except KeyError as e:
            raise KeyError(f'Unknown view `{name}`, available: {list(self.storage_view_impl.keys())}') from e

    def __call__(self) -> dict:
        return self.raw

    # FIXME: Unnecessary multiple JSON conversions
    @staticmethod
    def fix_metadata_json(metadata_json: Dict[str, Any]) -> Dict[str, Any]:
        """Fix invalid field casing in metadata JSON"""
        metadata_json_string = json.dumps(metadata_json)
        for from_, to in _metadata_json_replace_table.items():
            metadata_json_string = metadata_json_string.replace(from_, to)
        return json.loads(metadata_json_string)

    @staticmethod
    def validate_metadata_json(metadata_json: Dict[str, Any]) -> None:
        """Validate metadata JSON with JSONSchema"""
        jsonschema_validate(instance=metadata_json, schema=metadata_schema)

    @classmethod
    def from_json(cls, metadata_json: Dict[str, Any], context: Optional[ExecutionContext] = None) -> 'ContractMetadata':
        """Convert metadata from JSON object"""
        metadata_json = cls.fix_metadata_json(metadata_json)
        cls.validate_metadata_json(metadata_json)
        res = converter.structure(metadata_json, ContractMetadata)
        res.context = context if context else ExecutionContext()
        res.raw = metadata_json
        return res

    @classmethod
    def from_ipfs(cls, multihash: str, context: Optional[ExecutionContext] = None) -> 'ContractMetadata':
        """Fetch metadata from IPFS network by multihash"""
        context = context or ExecutionContext()
        metadata_json = requests.get(f'{context.ipfs_gateway}/{multihash}').json()
        return cls.from_json(metadata_json, context)

    @classmethod
    def from_url(cls, url: str, context: Optional[ExecutionContext] = None) -> 'ContractMetadata':
        """Fetch metadata from HTTP(S) URL"""
        metadata_json = requests.get(url).json()
        return cls.from_json(metadata_json, context)

    @classmethod
    def from_file(cls, path: str, context: Optional[ExecutionContext] = None) -> 'ContractMetadata':
        """Read metadata from JSON file by path"""
        with open(path) as f:
            metadata_json = json.load(f)
            return cls.from_json(metadata_json, context)
