from __future__ import annotations

import os
import time
import threading
from dataclasses import dataclass
from typing import Optional

from .alphabets import (
    ALPHABET_NO_0O1IUV,
    BASE_NO_0O1IUV,
    CHAR_TO_VAL_NO_0O1IUV,
)
from .currencies import CURRENCY_TO_ID, ID_TO_CURRENCY
from .exceptions import InvalidTxIdError, SequenceOverflowError, UnsupportedCurrencyError

# Custom epoch: 2026-01-01 00:00:00 UTC (keep fixed once deployed)
EPOCH_UTC_SECONDS = 1767225600

# Bit layout (50-bit payload, fits in 11 base-24 chars ~ 50.4 bits)
TS_BITS = 38   # 10ms ticks since epoch (~87 years)
NODE_BITS = 4  # 0..15
CURR_BITS = 4  # 0..15 (<=16 currencies)
SEQ_BITS = 4   # 0..15 per (node, 10ms tick)

TS_MASK = (1 << TS_BITS) - 1
NODE_MASK = (1 << NODE_BITS) - 1
CURR_MASK = (1 << CURR_BITS) - 1
SEQ_MASK = (1 << SEQ_BITS) - 1

SEQ_MAX = SEQ_MASK
CURR_MAX = CURR_MASK
NODE_MAX = NODE_MASK

SEQ_SHIFT = 0
CURR_SHIFT = SEQ_BITS
NODE_SHIFT = CURR_SHIFT + CURR_BITS
TS_SHIFT = NODE_SHIFT + NODE_BITS

TOTAL_BITS = TS_BITS + NODE_BITS + CURR_BITS + SEQ_BITS  # 50
PAYLOAD_LEN = 11
TOTAL_ID_LEN = 1 + PAYLOAD_LEN  # 12


def _encode_base24_fixed(value: int, length: int) -> str:
    if value < 0:
        raise InvalidTxIdError("value must be non-negative")
    out = [""] * length
    for i in range(length - 1, -1, -1):
        out[i] = ALPHABET_NO_0O1IUV[value % BASE_NO_0O1IUV]
        value //= BASE_NO_0O1IUV
    if value != 0:
        raise InvalidTxIdError("value does not fit in requested length")
    return "".join(out)


def _decode_base24(s: str) -> int:
    v = 0
    for ch in s:
        try:
            digit = CHAR_TO_VAL_NO_0O1IUV[ch]
        except KeyError as e:
            raise InvalidTxIdError(f"invalid char: {ch}") from e
        v = v * BASE_NO_0O1IUV + digit
    return v


def _unix_ns() -> int:
    return time.time_ns()


def _to_ts10(now_unix_ns: int) -> int:
    epoch_ns = EPOCH_UTC_SECONDS * 1_000_000_000
    if now_unix_ns < epoch_ns:
        raise InvalidTxIdError("current time is before configured epoch")
    return (now_unix_ns - epoch_ns) // 10_000_000  # 10ms


@dataclass(frozen=True)
class TxIdV2Parts:
    prefix: str
    ts10: int
    node: int
    currency_id: int
    seq: int

    @property
    def currency(self) -> str:
        return ID_TO_CURRENCY.get(self.currency_id, "UNKNOWN")

    @property
    def unix_seconds(self) -> int:
        return EPOCH_UTC_SECONDS + (self.ts10 // 100)

    @property
    def unix_ns_approx(self) -> int:
        return (EPOCH_UTC_SECONDS * 1_000_000_000) + (self.ts10 * 10_000_000)


class TxIdV2Generator:
    __slots__ = ("node_id", "_lock", "_last_ts10", "_seq")

    def __init__(self, node_id: int):
        if not (0 <= node_id <= NODE_MAX):
            raise ValueError(f"node_id must be 0..{NODE_MAX}")
        self.node_id = node_id
        self._lock = threading.Lock()
        self._last_ts10: Optional[int] = None
        self._seq: int = 0

    def generate(self, prefix: str, currency: str, now_unix_ns: Optional[int] = None) -> str:
        if not isinstance(prefix, str) or len(prefix) != 1 or not prefix.isalpha():
            raise ValueError("prefix must be a single alphabetic character")
        prefix = prefix.upper()

        currency = currency.upper()
        if currency not in CURRENCY_TO_ID:
            raise UnsupportedCurrencyError(f"unsupported currency: {currency}")
        currency_id = CURRENCY_TO_ID[currency]
        if currency_id > CURR_MAX:
            raise UnsupportedCurrencyError(
                f"currency_id must be 0..{CURR_MAX} for V2 layout; got {currency_id}"
            )

        if now_unix_ns is None:
            now_unix_ns = _unix_ns()
        ts10 = _to_ts10(now_unix_ns)
        if ts10 > TS_MASK:
            raise InvalidTxIdError("ts10 exceeded 38-bit range (epoch too old?)")

        with self._lock:
            # clamp backward time
            if self._last_ts10 is not None and ts10 < self._last_ts10:
                ts10 = self._last_ts10

            if ts10 != self._last_ts10:
                self._last_ts10 = ts10
                self._seq = 0
            else:
                self._seq += 1
                if self._seq > SEQ_MAX:
                    raise SequenceOverflowError(
                        "sequence overflow (>15 IDs in same 10ms tick on this node)"
                    )
            seq = self._seq

        payload = (
            ((ts10 & TS_MASK) << TS_SHIFT)
            | ((self.node_id & NODE_MASK) << NODE_SHIFT)
            | ((currency_id & CURR_MASK) << CURR_SHIFT)
            | (seq & SEQ_MASK)
        )
        # optional strictness
        if payload >= (1 << TOTAL_BITS):
            raise InvalidTxIdError("payload exceeds expected bit size")

        return prefix + _encode_base24_fixed(payload, PAYLOAD_LEN)


def parse_txid_v2(txid: str) -> TxIdV2Parts:
    if not isinstance(txid, str) or len(txid) != TOTAL_ID_LEN:
        raise InvalidTxIdError(f"txid must be exactly {TOTAL_ID_LEN} characters")

    prefix = txid[0]
    if not prefix.isalpha():
        raise InvalidTxIdError("txid must start with a letter")
    prefix = prefix.upper()

    payload_str = txid[1:]
    if len(payload_str) != PAYLOAD_LEN:
        raise InvalidTxIdError("invalid payload length")

    payload = _decode_base24(payload_str)
    if payload >= (1 << TOTAL_BITS):
        raise InvalidTxIdError("payload exceeds expected bit size")

    ts10 = (payload >> TS_SHIFT) & TS_MASK
    node = (payload >> NODE_SHIFT) & NODE_MASK
    currency_id = (payload >> CURR_SHIFT) & CURR_MASK
    seq = payload & SEQ_MASK

    return TxIdV2Parts(prefix=prefix, ts10=ts10, node=node, currency_id=currency_id, seq=seq)


def make_v2_generator_from_env(var_name: str = "NODE_ID") -> TxIdV2Generator:
    node_str = os.getenv(var_name)
    if node_str is None:
        raise RuntimeError(f"{var_name} not set (expected 0..{NODE_MAX})")
    try:
        node_id = int(node_str)
    except ValueError as e:
        raise RuntimeError(f"{var_name} must be an integer 0..{NODE_MAX}") from e
    return TxIdV2Generator(node_id=node_id)
