# This file contains code adapted from web3-ethereum-defi (https://github.com/Kartograf/web3-ethereum-defi)
# Licensed under the MIT License.
# Original copyright (c) 2023 Kartograf contributors.

# Additional modifications by Ian Moore, 2023–2025
# Licensed under the Apache License, Version 2.0

"""Bunch of random utilities."""
import calendar
import datetime
import logging
import os
import random
import socket
import time
from typing import Optional, Tuple
from urllib.parse import urlparse

import psutil


logger = logging.getLogger(__name__)

#: Ethereum 0x0000000000000000000000000000000000000000 address as a string
ZERO_ADDRESS_STR = "0x0000000000000000000000000000000000000000"    

class BaseUtils():

    def __init__(self):
        pass        

    def sanitise_string(self, s: str) -> str:
        """Remove null characters."""
        # https://stackoverflow.com/a/18762899/315168
        return s.replace("\x00", "\U0000FFFD")
    
    
    def is_localhost_port_listening(self, port: int, host="localhost") -> bool:
        """Check if a localhost is running a server already.
    
        See https://www.adamsmith.haus/python/answers/how-to-check-if-a-network-port-is-open-in-python
    
        :return: True if there is a process occupying the port
        """
    
        a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        location = (host, port)
        result_of_check = a_socket.connect_ex(location)
        return result_of_check == 0
    
    
    def find_free_port(self, min_port: int = 20_000, max_port: int = 40_000, max_attempt: int = 20) -> int:
        """Find a free localhost port to bind.
    
        Does by random.
    
        .. note ::
    
            Subject to race condition, but should be rareish.
    
        :param min_port:
            Minimum port range
    
        :param max_port:
            Maximum port range
    
        :param max_attempt:
            Give up and die with an exception if no port found after this many attempts.
    
        :return:
            Free port number
        """
    
        assert type(min_port) == int
        assert type(max_port) == int
        assert type(max_attempt) == int
    
        for attempt in range(0, max_attempt):
            random_port = random.randrange(start=min_port, stop=max_port)
            logger.info("Attempting to allocate port %d to Anvil", random_port)
            if not is_localhost_port_listening(random_port, "127.0.0.1"):
                return random_port
    
        raise RuntimeError(f"Could not open a port with a spec: {min_port} - {max_port}, {max_attempt} attempts")
    
    
    def shutdown_hard(
        self,
        process: psutil.Popen,
        log_level: Optional[int] = None,
        block=True,
        block_timeout=30,
        check_port: Optional[int] = None,
    ) -> Tuple[bytes, bytes]:
        """Kill Psutil process.
    
        - Straight out OS `SIGKILL` a process
    
        - Log output if necessary
    
        - Use port listening to check that the process goes down
          and frees its ports
    
        :param process:
            Process to kill
    
        :param block:
            Block the execution until the process has terminated.
    
            You must give `check_port` option to ensure we enforce the shutdown.
    
        :param block_timeout:
            How long we give for process to clean up after itself
    
        :param log_level:
            If set, dump anything in Anvil stdout to the Python logging using level `INFO`.
    
        :param check_port:
            Check that TCP/IP localhost port is freed after shutdown
    
        :return:
            stdout, stderr as string
        """
    
        stdout = b""
        stderr = b""
    
        if process.poll() is None:
            # Still alive, we need to kill to read the output
            process.kill()
    
        for line in process.stdout.readlines():
            stdout += line
            if log_level is not None:
                logger._log(log_level, "stdout: %s", line.decode("utf-8").strip())
    
        for line in process.stderr.readlines():
            stderr += line
            if log_level is not None:
                logger._log(log_level, "stderr: %s", line.decode("utf-8").strip())
    
        if block:
            assert check_port is not None, "Give check_port to block the execution"
            deadline = time.time() + 30
            while time.time() < deadline:
                if not is_localhost_port_listening(check_port):
                    # Port released, assume Anvil/Ganache is gone
                    return stdout, stderr
    
            raise AssertionError(f"Could not terminate Anvil in {block_timeout} seconds, stdout is %d bytes, stderr is %d bytes", len(stdout), len(stderr))
    
        return stdout, stderr
    
    
    def to_unix_timestamp(self, dt: datetime.datetime) -> float:
        """Convert Python UTC datetime to UNIX seconds since epoch.
    
        Example:
    
        .. code-block:: python
    
            import datetime
            from eth_defi.utils import to_unix_timestamp
    
            dt = datetime.datetime(1970, 1, 1)
            unix_time = to_unix_timestamp(dt)
            assert unix_time == 0
    
        :param dt:
            Python datetime to convert
    
        :return:
            Datetime as seconds since 1970-1-1
        """
        # https://stackoverflow.com/a/5499906/315168
        return calendar.timegm(dt.utctimetuple())
    
    
    def get_url_domain(self, url: str) -> str:
        """Redact URL so that only domain is displayed.
    
        Some services e.g. infura use path as an API key.
        """
        parsed = urlparse(url)
        if parsed.port in (80, 443, None):
            return parsed.hostname
        else:
            return f"{parsed.hostname}:{parsed.port}"
    
    
    def setup_console_logging(self):
        """Set up coloured log output.
    
        - Helper function to have nicer logging output in tutorial scripts.
        - Tune down some noisy dependency library logging
        """
    
        try:
            import coloredlogs
        except ImportError as e:
            raise RuntimeError("coloredlogs package missing - please install with pip first before running") from e
    
        level = os.environ.get("LOG_LEVEL", "info").upper()
    
        fmt = "%(asctime)s %(name)-44s %(message)s"
        date_fmt = "%H:%M:%S"
        coloredlogs.install(level=level, fmt=fmt, date_fmt=date_fmt)
    
        logging.basicConfig(level=level, handlers=[logging.StreamHandler()])
    
        # Mute noise
        logging.getLogger("web3.providers.HTTPProvider").setLevel(logging.WARNING)
        logging.getLogger("web3.RequestManager").setLevel(logging.WARNING)
        logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
