import base64
import time
import json
import hashlib
import hmac
import os
from datetime import datetime, timezone, timedelta
from typing import Optional
from nacl import bindings
from nacl.signing import SigningKey
from nacl.public import PrivateKey
from dataclasses import dataclass

def b64e(b: bytes) -> str:
    return base64.b64encode(b).decode("ascii")

def b64d(s: str) -> bytes:
    return base64.b64decode(s.encode("ascii"))

def now_ts() -> int:
    return int(time.time())

def canonical_json(obj) -> str:
    return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)

def sha256_hex(b: bytes) -> str:
    return hashlib.sha256(b).hexdigest()

_time_display_settings = {
    "timezone": "UTC",
    "time_format": "24h",
}

def set_time_display_settings(tz: str = "UTC", fmt: str = "24h") -> None:
    _time_display_settings["timezone"] = tz or "UTC"
    _time_display_settings["time_format"] = fmt if fmt in ("12h", "24h") else "24h"

def get_time_display_settings() -> dict:
    return dict(_time_display_settings)

def _get_display_tz() -> Optional[timezone]:
    tz_str = _time_display_settings.get("timezone", "UTC")

    if tz_str == "UTC":
        return timezone.utc

    if tz_str == "local":

        return None

    try:
        tz_str = tz_str.strip()
        if tz_str.startswith("+") or tz_str.startswith("-"):
            sign = 1 if tz_str[0] == "+" else -1
            rest = tz_str[1:].replace(":", "")
            if len(rest) == 4:
                hours = int(rest[:2])
                mins = int(rest[2:])
            elif len(rest) == 2:
                hours = int(rest)
                mins = 0
            else:
                return timezone.utc
            offset = timedelta(hours=sign * hours, minutes=sign * mins)
            return timezone(offset)
    except Exception:
        pass

    return timezone.utc

def _format_time_str(dt: datetime) -> str:
    fmt = _time_display_settings.get("time_format", "24h")
    if fmt == "12h":
        return dt.strftime("%I:%M %p").lstrip("0")
    return dt.strftime("%H:%M")

def _format_date_str(dt: datetime) -> str:
    return dt.strftime("%Y-%m-%d")

def fmt_time(ts: int) -> str:
    try:
        utc_dt = datetime.fromtimestamp(ts, tz=timezone.utc)
        display_tz = _get_display_tz()

        if display_tz is None:

            local_dt = datetime.fromtimestamp(ts)
            return _format_time_str(local_dt)

        display_dt = utc_dt.astimezone(display_tz)
        return _format_time_str(display_dt)
    except Exception:
        return time.strftime("%H:%M", time.gmtime(ts))

def fmt_date(ts: int) -> str:
    try:
        utc_dt = datetime.fromtimestamp(ts, tz=timezone.utc)
        display_tz = _get_display_tz()

        if display_tz is None:

            local_dt = datetime.fromtimestamp(ts)
            return _format_date_str(local_dt)

        display_dt = utc_dt.astimezone(display_tz)
        return _format_date_str(display_dt)
    except Exception:
        return time.strftime("%Y-%m-%d", time.gmtime(ts))

def fmt_datetime(ts: int) -> str:
    return f"{fmt_date(ts)} {fmt_time(ts)}"

def fmt_datetime_with_tz(ts: int) -> str:
    tz_str = _time_display_settings.get("timezone", "UTC")
    if tz_str == "local":
        suffix = "local"
    elif tz_str == "UTC":
        suffix = "UTC"
    else:
        suffix = tz_str
    return f"{fmt_datetime(ts)} ({suffix})"

TIMEZONE_OPTIONS = [
    ("UTC", "UTC (Coordinated Universal Time)"),
    ("local", "Local (System Time)"),
    ("-12:00", "UTC-12:00 (Baker Island)"),
    ("-11:00", "UTC-11:00 (Samoa)"),
    ("-10:00", "UTC-10:00 (Hawaii)"),
    ("-09:00", "UTC-09:00 (Alaska)"),
    ("-08:00", "UTC-08:00 (Pacific)"),
    ("-07:00", "UTC-07:00 (Mountain)"),
    ("-06:00", "UTC-06:00 (Central)"),
    ("-05:00", "UTC-05:00 (Eastern)"),
    ("-04:00", "UTC-04:00 (Atlantic)"),
    ("-03:00", "UTC-03:00 (Buenos Aires)"),
    ("-02:00", "UTC-02:00"),
    ("-01:00", "UTC-01:00 (Azores)"),
    ("+00:00", "UTC+00:00 (London/Lisbon)"),
    ("+01:00", "UTC+01:00 (Berlin/Paris)"),
    ("+02:00", "UTC+02:00 (Cairo/Athens)"),
    ("+03:00", "UTC+03:00 (Moscow/Istanbul)"),
    ("+04:00", "UTC+04:00 (Dubai)"),
    ("+05:00", "UTC+05:00 (Karachi)"),
    ("+05:30", "UTC+05:30 (Mumbai)"),
    ("+06:00", "UTC+06:00 (Dhaka)"),
    ("+07:00", "UTC+07:00 (Bangkok)"),
    ("+08:00", "UTC+08:00 (Singapore/Beijing)"),
    ("+09:00", "UTC+09:00 (Tokyo/Seoul)"),
    ("+10:00", "UTC+10:00 (Sydney)"),
    ("+11:00", "UTC+11:00"),
    ("+12:00", "UTC+12:00 (Auckland)"),
]

TIME_FORMAT_OPTIONS = [
    ("24h", "24-hour (14:30)"),
    ("12h", "12-hour (2:30 PM)"),
]

def hkdf_extract(salt: bytes, ikm: bytes) -> bytes:
    return hmac.new(salt, ikm, hashlib.sha256).digest()

def hkdf_expand(prk: bytes, info: bytes, length: int) -> bytes:
    out = b""
    t = b""
    c = 1
    while len(out) < length:
        t = hmac.new(prk, t + info + bytes([c]), hashlib.sha256).digest()
        out += t
        c += 1
    return out[:length]

def hkdf(salt: bytes, ikm: bytes, info: bytes, length: int) -> bytes:
    prk = hkdf_extract(salt, ikm)
    return hkdf_expand(prk, info, length)

def kdf_chain(chain_key: bytes) -> tuple[bytes, bytes]:
    mk = hmac.new(chain_key, b"msg", hashlib.sha256).digest()
    ck = hmac.new(chain_key, b"ck", hashlib.sha256).digest()
    return ck, mk

def aead_encrypt(key32: bytes, plaintext: bytes, aad: bytes) -> tuple[bytes, bytes]:
    nonce = os.urandom(24)
    ct = bindings.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, aad, nonce, key32)
    return nonce, ct

def aead_decrypt(key32: bytes, nonce: bytes, ciphertext: bytes, aad: bytes) -> bytes:
    return bindings.crypto_aead_xchacha20poly1305_ietf_decrypt(ciphertext, aad, nonce, key32)

def short_snip(text: str, max_chars: int = 80) -> str:
    t = text.replace("\n", " ").strip()
    return (t[:max_chars] + "…") if len(t) > max_chars else t

@dataclass
class Identity:
    alias: str
    sign_sk: SigningKey
    dh_sk: PrivateKey

    @property
    def sign_pub(self) -> bytes:
        return bytes(self.sign_sk.verify_key)

    @property
    def dh_pub(self) -> bytes:
        return bytes(self.dh_sk.public_key)

    @property
    def fp(self) -> str:
        h = sha256_hex(self.sign_pub)
        return f"{h[:4]}-{h[4:8]}-{h[8:12]}-{h[12:16]}"

    @staticmethod
    def load_or_create(db, alias: str) -> "Identity":
        row = db.load_identity(alias)
        if row:
            _, sign_sk_b64, dh_sk_b64 = row
            return Identity(alias, SigningKey(b64d(sign_sk_b64)), PrivateKey(b64d(dh_sk_b64)))
        ident = Identity(alias, SigningKey.generate(), PrivateKey.generate())
        db.save_identity(alias, b64e(bytes(ident.sign_sk)), b64e(bytes(ident.dh_sk)))
        return ident

@dataclass
class Bundle:
    alias: str
    fp: str
    sign_pub: bytes
    dh_pub: bytes
    pinned_fp: str = None

@dataclass
class Session:
    peer_alias: str
    peer_fp: str
    send_ck: bytes
    recv_ck: bytes
    send_n: int = 0
    recv_n: int = 0
