#   -*- coding: utf-8 -*-
#
#   This file is part of SKALE.py
#
#   Copyright (C) 2019-Present SKALE Labs
#
#   SKALE.py is free software: you can redistribute it and/or modify
#   it under the terms of the GNU Affero General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   SKALE.py is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU Affero General Public License for more details.
#
#   You should have received a copy of the GNU Affero General Public License
#   along with SKALE.py.  If not, see <https://www.gnu.org/licenses/>.

import logging
import time
from typing import Any, Dict, Iterable
from urllib.parse import urlparse

import requests
from eth_keys.main import lazy_key_api as keys
from eth_typing import Address, AnyAddress, ChecksumAddress, HexStr
from requests.exceptions import ConnectionError  # type: ignore
from web3 import HTTPProvider, LegacyWebSocketProvider, Web3
from web3.exceptions import ProviderConnectionError, StaleBlockchain, TransactionNotFound
from web3.middleware import AttributeDictMiddleware, Middleware, StalecheckMiddlewareBuilder
from web3.providers.base import JSONBaseProvider
from web3.types import ENS, Nonce, TxReceipt, _Hash32

import skale.config as config
from skale.transactions.exceptions import TransactionFailedError, TransactionNotMinedError
from skale.utils.cache import RedisCacheConfig, redis_cache_middleware
from skale.utils.constants import GAS_PRICE_COEFFICIENT
from skale.utils.exceptions import NoSyncedEndpointError

logger = logging.getLogger(__name__)


WS_MAX_MESSAGE_DATA_BYTES = 5 * 1024 * 1024
MAX_WAITING_TIME = 3 * 60 * 60  # 3 hours
BLOCK_WAITING_TIMEOUT = 1
DEFAULT_HTTP_TIMEOUT = 120
DEFAULT_BLOCKS_TO_WAIT = 50


def get_provider(
    endpoint: str,
    timeout: int = DEFAULT_HTTP_TIMEOUT,
    request_kwargs: Dict[str, Any] | None = None,
    session: Any = None,
) -> JSONBaseProvider:
    scheme = urlparse(endpoint).scheme
    if scheme == 'ws' or scheme == 'wss':
        kwargs = request_kwargs or {'max_size': WS_MAX_MESSAGE_DATA_BYTES}
        return LegacyWebSocketProvider(endpoint, websocket_timeout=timeout, websocket_kwargs=kwargs)

    if scheme == 'http' or scheme == 'https':
        kwargs = {'timeout': timeout, **(request_kwargs or {})}
        return HTTPProvider(endpoint, session=session, request_kwargs=kwargs)

    raise Exception('Wrong endpoint option.Supported endpoint schemes: http/https/ws/wss')


def init_web3(
    endpoint: str,
    provider_timeout: int = DEFAULT_HTTP_TIMEOUT,
    middlewares: Iterable[Middleware] | None = None,
    cache_config: RedisCacheConfig | None = None,
    ts_diff: int | None = None,
    session: requests.Session | None = None,
) -> Web3:
    provider = get_provider(
        endpoint,
        timeout=provider_timeout,
        session=session,
    )
    provider.cache_allowed_requests = True
    w3 = Web3(provider)
    if not middlewares:
        ts_diff = ts_diff or config.ALLOWED_TS_DIFF
        if not ts_diff == config.NO_SYNC_TS_DIFF:
            stalecheck_middleware = StalecheckMiddlewareBuilder.build(ts_diff)
            middlewares = [stalecheck_middleware, AttributeDictMiddleware]
        else:
            middlewares = [AttributeDictMiddleware]

    if cache_config is not None:
        caching_middleware = redis_cache_middleware(cache_config)
        middlewares_list = list(middlewares)
        try:
            attribute_index = middlewares_list.index(AttributeDictMiddleware)
        except ValueError:
            attribute_index = len(middlewares_list)
        middlewares_list.insert(attribute_index, caching_middleware)
        middlewares = middlewares_list

    for middleware in middlewares:
        w3.middleware_onion.add(middleware)
    return w3


def get_endpoint(
    endpoint: str | list[str],
    provider_timeout: int = DEFAULT_HTTP_TIMEOUT,
    ts_diff: int | None = None,
) -> str:
    if isinstance(endpoint, str):
        return endpoint
    elif isinstance(endpoint, list) and len(endpoint) > 0:
        return _get_connected_endpoint(endpoint, provider_timeout=provider_timeout, ts_diff=ts_diff)
    else:
        raise ValueError('Endpoint must be a string or a non-empty list of strings.')


def _get_connected_endpoint(
    endpoints: list[str], provider_timeout: int, ts_diff: int | None
) -> str:
    for url in endpoints:
        try:
            w3 = init_web3(url, provider_timeout=provider_timeout, ts_diff=ts_diff)
            if w3.eth.block_number:
                return url
        except (ProviderConnectionError, ConnectionError) as e:
            logger.warning(f'Could not connect to {url}. Error: {e}. Trying next endpoint...')
            time.sleep(2)
        except StaleBlockchain as e:
            logger.warning(f'Endpoint {url} is out of sync. Error: {e}. Trying next endpoint...')
            time.sleep(2)
    raise NoSyncedEndpointError(f'Could not connect to any RPC endpoints: {endpoints}')


def get_receipt(web3: Web3, tx: _Hash32) -> TxReceipt:
    return web3.eth.get_transaction_receipt(tx)


def get_eth_nonce(web3: Web3, address: Address | ChecksumAddress | ENS) -> Nonce:
    return web3.eth.get_transaction_count(address)


def wait_for_receipt_by_blocks(
    web3: Web3,
    tx: _Hash32,
    blocks_to_wait: int = DEFAULT_BLOCKS_TO_WAIT,
    timeout: int = MAX_WAITING_TIME,
) -> TxReceipt:
    blocks_to_wait = blocks_to_wait or DEFAULT_BLOCKS_TO_WAIT
    timeout = timeout or MAX_WAITING_TIME
    previous_block = web3.eth.block_number
    current_block = previous_block
    wait_start_time = time.time()
    while (
        time.time() - wait_start_time < timeout and current_block <= previous_block + blocks_to_wait
    ):
        try:
            receipt = get_receipt(web3, tx)
        except TransactionNotFound:
            receipt = None
        if receipt is not None:
            return receipt
        current_block = web3.eth.block_number
        time.sleep(3)
    raise TransactionNotMinedError(
        f'Transaction with hash: {str(tx)} not found in {blocks_to_wait} blocks.'
    )


def wait_receipt(web3: Web3, tx: _Hash32, retries: int = 30, timeout: int = 5) -> TxReceipt:
    for _ in range(0, retries):
        try:
            receipt = get_receipt(web3, tx)
        except TransactionNotFound:
            receipt = None
        if receipt is not None:
            return receipt
        time.sleep(timeout)  # pragma: no cover
    raise TransactionNotMinedError(
        f'Transaction with hash: {str(tx)} not mined after {retries} retries.'
    )


def check_receipt(receipt: TxReceipt, raise_error: bool = True) -> bool:
    if receipt['status'] != 1:  # pragma: no cover
        if raise_error:
            raise TransactionFailedError(f'Transaction failed, see receipt {receipt}')
        else:
            return False
    return True


def wait_for_confirmation_blocks(
    web3: Web3, blocks_to_wait: int, timeout: int = MAX_WAITING_TIME, request_timeout: int = 5
) -> None:
    current_block = start_block = web3.eth.block_number
    logger.info(
        f'Current block number is {current_block}, '
        f'waiting for {blocks_to_wait} confimration blocks to be mined'
    )
    wait_start_time = time.time()
    while time.time() - wait_start_time < timeout and current_block <= start_block + blocks_to_wait:
        current_block = web3.eth.block_number
        time.sleep(request_timeout)


def private_key_to_public(pr: HexStr) -> HexStr:
    pr_bytes = Web3.to_bytes(hexstr=pr)
    prk = keys.PrivateKey(pr_bytes)
    pk = prk.public_key
    return HexStr(pk.to_hex())


def public_key_to_address(pk: HexStr) -> HexStr:
    hash = Web3.keccak(hexstr=str(pk))
    return Web3.to_hex(hash[-20:])


def private_key_to_address(pr: HexStr) -> HexStr:
    pk = private_key_to_public(pr)
    return public_key_to_address(pk)


def to_checksum_address(address: AnyAddress | str | bytes) -> ChecksumAddress:
    return Web3.to_checksum_address(address)


def default_gas_price(web3: Web3) -> int:
    return web3.eth.gas_price * GAS_PRICE_COEFFICIENT
