from __future__ import annotations

import calendar
import hashlib
import json
import sqlite3
import time
import typing as tp
from datetime import date
from email.utils import parsedate_tz
from typing import Any, AsyncIterator, Generator, Iterable, Iterator, TypeVar

import anyio
import anysqlite
import httpcore
import httpx
from anyio import from_thread, to_thread

HEADERS_ENCODING = "iso-8859-1"

T = tp.TypeVar("T")


class BaseClock:
    def now(self) -> int:
        raise NotImplementedError()


class Clock(BaseClock):
    def now(self) -> int:
        return int(time.time())


def normalized_url(url: tp.Union[httpcore.URL, str, bytes]) -> str:
    if isinstance(url, str):  # pragma: no cover
        return url

    if isinstance(url, bytes):  # pragma: no cover
        return url.decode("ascii")

    if isinstance(url, httpcore.URL):
        port = f":{url.port}" if url.port is not None else ""
        return f"{url.scheme.decode('ascii')}://{url.host.decode('ascii')}{port}{url.target.decode('ascii')}"
    assert False, "Invalid type for `normalized_url`"  # pragma: no cover


def get_safe_url(url: httpcore.URL) -> str:
    httpx_url = httpx.URL(bytes(url).decode("ascii"))

    schema = httpx_url.scheme
    host = httpx_url.host
    path = httpx_url.path

    return f"{schema}://{host}{path}"


def generate_key(request: httpcore.Request, body: bytes = b"") -> str:
    encoded_url = normalized_url(request.url).encode("ascii")

    key_parts = [request.method, encoded_url, body]

    # FIPs mode disables blake2 algorithm, use sha256 instead when not found.
    blake2b_hasher = None
    sha256_hasher = hashlib.sha256(usedforsecurity=False)
    try:
        blake2b_hasher = hashlib.blake2b(digest_size=16, usedforsecurity=False)
    except (ValueError, TypeError, AttributeError):
        pass

    hexdigest: str
    if blake2b_hasher:
        for part in key_parts:
            blake2b_hasher.update(part)

        hexdigest = blake2b_hasher.hexdigest()
    else:
        for part in key_parts:
            sha256_hasher.update(part)

        hexdigest = sha256_hasher.hexdigest()
    return hexdigest


def extract_header_values(
    headers: tp.List[tp.Tuple[bytes, bytes]],
    header_key: tp.Union[bytes, str],
    single: bool = False,
) -> tp.List[bytes]:
    if isinstance(header_key, str):
        header_key = header_key.encode(HEADERS_ENCODING)
    extracted_headers = []
    for key, value in headers:
        if key.lower() == header_key.lower():
            extracted_headers.append(value)
            if single:
                break
    return extracted_headers


def extract_header_values_decoded(
    headers: tp.List[tp.Tuple[bytes, bytes]], header_key: bytes, single: bool = False
) -> tp.List[str]:
    values = extract_header_values(headers=headers, header_key=header_key, single=single)
    return [value.decode(HEADERS_ENCODING) for value in values]


def header_presents(headers: tp.List[tp.Tuple[bytes, bytes]], header_key: bytes) -> bool:
    return bool(extract_header_values(headers, header_key, single=True))


def parse_date(date: str) -> tp.Optional[int]:
    expires = parsedate_tz(date)
    if expires is None:
        return None
    timestamp = calendar.timegm(expires[:6])
    return timestamp


async def asleep(seconds: tp.Union[int, float]) -> None:
    await anyio.sleep(seconds)


def sleep(seconds: tp.Union[int, float]) -> None:
    time.sleep(seconds)


def float_seconds_to_int_milliseconds(seconds: float) -> int:
    return int(seconds * 1000)


def partition(iterable: tp.Iterable[T], predicate: tp.Callable[[T], bool]) -> tp.Tuple[tp.List[T], tp.List[T]]:
    """
    Partition an iterable into two lists: one for matching items and one for non-matching items.

    Args:
        iterable (tp.Iterable[T]): The input iterable to partition.
        predicate (tp.Callable[[T], bool]): A function that evaluates each item in the iterable.

    Returns:
        tp.Tuple[tp.List[T], tp.List[T]]: A tuple containing two lists: the first for matching items,
        and the second for non-matching items.
    Example:
        ```
        iterable = [1, 2, 3, 4, 5]
        is_even = lambda x: x % 2 == 0
        evens, odds = partition(iterable, is_even)
        ```
    """
    matching, non_matching = [], []
    for item in iterable:
        if predicate(item):
            matching.append(item)
        else:
            non_matching.append(item)
    return matching, non_matching


def async_iterator_to_sync(iterator: AsyncIterator[bytes]) -> Iterator[bytes]:
    """
    Convert an asynchronous byte iterator to a synchronous one.
    This function takes an asynchronous iterator that yields bytes and converts it into
    a synchronous iterator.

    Args:
        iterator (AsyncIterator[bytes]): The asynchronous byte iterator to be converted.
    Returns:
        Iterator[bytes]: A synchronous iterator that yields the same byte chunks as the input iterator.
    Example:
        ```python
        async_iter = some_async_byte_stream()
        sync_iter = async_iterator_to_sync(async_iter)
        for chunk in sync_iter:
            process_bytes(chunk)
        ```
    """

    while True:
        try:
            chunk = from_thread.run(iterator.__anext__)
        except StopAsyncIteration:
            break
        yield chunk


def _call_next(iterator: Iterator[bytes]) -> bytes:
    try:
        return iterator.__next__()
    except StopIteration:
        raise StopAsyncIteration


async def sync_iterator_to_async(iterator: Iterator[bytes]) -> AsyncIterator[bytes]:
    """
    Converts a synchronous bytes iterator to an asynchronous one.
    This function takes a synchronous iterator that yields bytes and converts it into an
    asynchronous iterator, allowing it to be used in async contexts without blocking.
    Args:
        iterator (Iterator[bytes]): A synchronous iterator yielding bytes objects.
    Returns:
        AsyncIterator[bytes]: An asynchronous iterator yielding the same bytes objects.
    Example:
        ```
        sync_iter = iter([b'data1', b'data2'])
        async for chunk in sync_iterator_to_async(sync_iter):
            await process_chunk(chunk)
        ```
    """

    while True:
        try:
            chunk = await to_thread.run_sync(_call_next, iterator)
        except StopAsyncIteration:
            break

        yield chunk


async def make_async_iterator(iterable: Iterable[bytes]) -> AsyncIterator[bytes]:
    for item in iterable:
        yield item


def make_sync_iterator(iterable: Iterable[bytes]) -> Iterator[bytes]:
    for item in iterable:
        yield item


def snake_to_header(text: str) -> str:
    """
    Convert snake_case string to Header-Case format.

    Args:
        text: Snake case string (e.g., "hishel_ttl")

    Returns:
        Header case string (e.g., "X-Hishel-Ttl")

    Examples:
        >>> snake_to_header("hishel_ttl")
        'X-Hishel-Ttl'
        >>> snake_to_header("cache_control")
        'X-Cache-Control'
        >>> snake_to_header("content_type")
        'X-Content-Type'
    """
    # Split by underscore, capitalize each word, join with dash, add X- prefix
    return "X-" + "-".join(word.capitalize() for word in text.split("_"))


_T = TypeVar("_T")


class GeneratorWithReturnValue:
    def __init__(
        self, gen: Generator[None, bytes | None, bytes], stream: AsyncIterator[bytes] | Iterator[bytes]
    ) -> None:
        self.gen = gen
        self.stream = stream
        self.value: bytes | None = None

    def __iter__(self) -> Iterator[bytes]:
        return self

    def __next__(self) -> bytes:
        assert isinstance(self.stream, Iterator)

        try:
            chunk = next(self.stream)
            self.gen.send(chunk)
        except StopIteration as exc:
            self.gen.send(None)
            self.value = exc.value
            raise
        return chunk

    def __aiter__(self) -> AsyncIterator[bytes]:
        return self

    async def __anext__(self) -> bytes:
        assert isinstance(self.stream, AsyncIterator)
        try:
            chunk = await self.stream.__anext__()
            self.gen.send(chunk)
        except StopIteration as exc:
            self.gen.send(None)
            self.value = exc.value
            raise
        return chunk


def print_sqlite_state(conn: sqlite3.Connection) -> str:
    """
    Print all tables and their rows in a pretty format suitable for inline snapshots.

    Args:
        conn: SQLite database connection

    Returns:
        Formatted string representation of the database state
    """
    cursor = conn.cursor()

    # Get all table names
    cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
    tables = [row[0] for row in cursor.fetchall()]

    output_lines = []
    output_lines.append("=" * 80)
    output_lines.append("DATABASE SNAPSHOT")
    output_lines.append("=" * 80)

    for table_name in tables:
        # Get column information
        cursor.execute(f"PRAGMA table_info({table_name})")
        columns = cursor.fetchall()
        column_names = [col[1] for col in columns]
        column_types = {col[1]: col[2] for col in columns}

        # Get all rows
        cursor.execute(f"SELECT * FROM {table_name}")
        rows = cursor.fetchall()

        output_lines.append("")
        output_lines.append(f"TABLE: {table_name}")
        output_lines.append("-" * 80)
        output_lines.append(f"Rows: {len(rows)}")
        output_lines.append("")

        if not rows:
            output_lines.append("  (empty)")
            continue

        # Format each row
        for idx, row in enumerate(rows, 1):
            output_lines.append(f"  Row {idx}:")

            for col_name, value in zip(column_names, row):
                col_type = column_types[col_name]
                formatted_value = format_value(value, col_name, col_type)
                output_lines.append(f"    {col_name:15} = {formatted_value}")

            if idx < len(rows):
                output_lines.append("")

    output_lines.append("")
    output_lines.append("=" * 80)

    result = "\n".join(output_lines)
    return result


async def aprint_sqlite_state(conn: anysqlite.Connection) -> str:
    """
    Print all tables and their rows in a pretty format suitable for inline snapshots.

    Args:
        conn: SQLite database connection

    Returns:
        Formatted string representation of the database state
    """
    cursor = await conn.cursor()

    # Get all table names
    await cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
    tables = [row[0] for row in await cursor.fetchall()]

    output_lines = []
    output_lines.append("=" * 80)
    output_lines.append("DATABASE SNAPSHOT")
    output_lines.append("=" * 80)

    for table_name in tables:
        # Get column information
        await cursor.execute(f"PRAGMA table_info({table_name})")
        columns = await cursor.fetchall()
        column_names = [col[1] for col in columns]
        column_types = {col[1]: col[2] for col in columns}

        # Get all rows
        await cursor.execute(f"SELECT * FROM {table_name}")
        rows = await cursor.fetchall()

        output_lines.append("")
        output_lines.append(f"TABLE: {table_name}")
        output_lines.append("-" * 80)
        output_lines.append(f"Rows: {len(rows)}")
        output_lines.append("")

        if not rows:
            output_lines.append("  (empty)")
            continue

        # Format each row
        for idx, row in enumerate(rows, 1):
            output_lines.append(f"  Row {idx}:")

            for col_name, value in zip(column_names, row):
                col_type = column_types[col_name]
                formatted_value = format_value(value, col_name, col_type)
                output_lines.append(f"    {col_name:15} = {formatted_value}")

            if idx < len(rows):
                output_lines.append("")

    output_lines.append("")
    output_lines.append("=" * 80)

    result = "\n".join(output_lines)
    return result


def format_value(value: Any, col_name: str, col_type: str) -> str:
    """Format a value for display based on its type and column name."""

    if value is None:
        return "NULL"

    # Handle BLOB columns
    if col_type.upper() == "BLOB":
        if isinstance(value, bytes):
            # Try to decode as UTF-8 string first
            try:
                decoded = value.decode("utf-8")
                # Check if it looks like JSON
                if decoded.strip().startswith("{") or decoded.strip().startswith("["):
                    try:
                        parsed = json.loads(decoded)
                        return f"(JSON) {json.dumps(parsed, indent=2)}"
                    except json.JSONDecodeError:
                        pass
                # Show string if it's printable
                if all(32 <= ord(c) <= 126 or c in "\n\r\t" for c in decoded):
                    return f"(str) '{decoded}'"
            except UnicodeDecodeError:
                pass

            # Show hex representation for binary data
            hex_str = value.hex()
            if len(hex_str) > 64:
                return f"(bytes) 0x{hex_str[:60]}... ({len(value)} bytes)"
            return f"(bytes) 0x{hex_str} ({len(value)} bytes)"
        return repr(value)

    # Handle timestamps - ONLY show date, not the raw timestamp
    if col_name.endswith("_at") and isinstance(value, (int, float)):
        try:
            dt = date.fromtimestamp(value)
            return dt.isoformat()  # Changed: removed the timestamp prefix
        except (ValueError, OSError):
            return str(value)

    # Handle TEXT columns
    if col_type.upper() == "TEXT":
        return f"'{value}'"

    # Handle other types
    return str(value)
