"""Licenciamento offline (machine-bound) e armazenamento criptografado de credenciais locais."""
import hashlib
import hmac
import json
import secrets
import time
from datetime import date, timedelta
from pathlib import Path

from cryptography.fernet import Fernet, InvalidToken

from .config import INSTANCE_DIR, DEFAULT_SYNC_INTERVAL_MINUTES

SECRET_KEY_FILE = INSTANCE_DIR / "secret.key"
LICENSE_FILE = INSTANCE_DIR / "license.json"
BLOCK_FLAG_FILE = INSTANCE_DIR / "bloqueado.flag"
CREDENTIALS_FILE = INSTANCE_DIR / "geotab_credentials.enc"
SETTINGS_FILE = INSTANCE_DIR / "settings.json"
CLOCK_FLOOR_FILE = INSTANCE_DIR / "clock_floor.json"
SESSION_SECRET_FILE = INSTANCE_DIR / "session_secret.key"

# Chave usada para assinar/validar licencas. Fica embutida no app entregue ao
# cliente (protecao basica, nao e criptografia de nivel militar) e deve ser a
# MESMA usada pela ferramenta interna que gera as licencas (vendor_tools/gerar_licenca.py).
LICENSE_SIGNING_SECRET = b"frota-trucks-license-v1-8f3a1c9d6e2b4f7a0c5d8e1b3f6a9c2d"

# Epoca fixa usada so pra compactar a data de validade em 4 digitos hex.
LICENSE_EPOCH = date(2026, 1, 1)


def check_and_update_clock_floor() -> bool:
    """
    Anti-rollback simples: guarda a maior data ja vista rodando o programa.
    Retorna False se a data atual do sistema for ANTERIOR a esse piso — sinal de
    que o relogio foi atrasado de proposito pra burlar validade/tolerancia offline.
    Sempre que a data atual for igual ou posterior ao piso, o piso avanca.
    """
    today_str = date.today().isoformat()
    floor_str = None
    if CLOCK_FLOOR_FILE.exists():
        try:
            floor_str = json.loads(CLOCK_FLOOR_FILE.read_text(encoding="utf-8")).get("floor_date")
        except Exception:
            floor_str = None

    if floor_str and today_str < floor_str:
        return False

    if today_str != floor_str:
        CLOCK_FLOOR_FILE.write_text(json.dumps({"floor_date": today_str}), encoding="utf-8")
    return True


def _sign(machine_id: str, expires_code: int) -> int:
    payload = f"{machine_id}|{expires_code}"
    digest = hmac.new(LICENSE_SIGNING_SECRET, payload.encode(), hashlib.sha256).hexdigest()
    return int(digest, 16) % (10 ** 12)


def generate_license(machine_id: str, expires_epoch: int) -> str:
    """
    Gera uma chave curta so com digitos, formato XXXX-XXXX-XXXX-XXXX.
    expires_epoch=0 significa sem expiracao.
    """
    expires_code = 0 if expires_epoch == 0 else min((date.fromtimestamp(expires_epoch) - LICENSE_EPOCH).days, 9999)
    sig = _sign(machine_id, expires_code)
    raw = f"{expires_code:04d}{sig:012d}"
    return "-".join(raw[i:i + 4] for i in range(0, len(raw), 4))


def validate_license_key(machine_id: str, license_key: str):
    """Retorna (ok, info_ou_erro). info contem expires_epoch quando ok."""
    if not check_and_update_clock_floor():
        return False, "A data do sistema parece ter sido atrasada. Ajuste a data/hora do Windows e tente novamente."

    raw = license_key.strip().replace("-", "").replace(" ", "")
    if len(raw) != 16 or not raw.isdigit():
        return False, "Chave de licença inválida (formato incorreto)."

    expires_code = int(raw[:4])
    sig_given = int(raw[4:])

    expected_sig = _sign(machine_id, expires_code)
    if sig_given != expected_sig:
        return False, "Chave de licença inválida (não confere com esta máquina)."

    if expires_code != 0:
        expires_date = LICENSE_EPOCH + timedelta(days=expires_code)
        if date.today() > expires_date:
            return False, "Chave de licença expirada."
        expires_epoch = int(time.mktime(expires_date.timetuple()))
    else:
        expires_epoch = 0

    return True, {"machine_id": machine_id, "expires_epoch": expires_epoch}


def save_license(license_key: str, company: str, info: dict):
    LICENSE_FILE.write_text(
        json.dumps({"license_key": license_key, "company": company, **info}, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )


def load_active_license():
    """Le a licenca salva e revalida. Retorna info dict (com company) se valida, senao None."""
    if BLOCK_FLAG_FILE.exists():
        return None
    if not LICENSE_FILE.exists():
        return None
    try:
        data = json.loads(LICENSE_FILE.read_text(encoding="utf-8"))
    except Exception:
        return None
    machine_id = data.get("machine_id", "")
    ok, info = validate_license_key(machine_id, data.get("license_key", ""))
    if not ok:
        return None
    return {**info, "company": data.get("company", "")}


def _get_or_create_fernet() -> Fernet:
    if SECRET_KEY_FILE.exists():
        key = SECRET_KEY_FILE.read_bytes()
    else:
        key = Fernet.generate_key()
        SECRET_KEY_FILE.write_bytes(key)
    return Fernet(key)


def save_geotab_credentials(username: str, password: str, database: str, server: str):
    fernet = _get_or_create_fernet()
    payload = json.dumps({
        "username": username, "password": password,
        "database": database, "server": server,
    }).encode()
    CREDENTIALS_FILE.write_bytes(fernet.encrypt(payload))


def load_geotab_credentials():
    if not CREDENTIALS_FILE.exists():
        return None
    fernet = _get_or_create_fernet()
    try:
        payload = fernet.decrypt(CREDENTIALS_FILE.read_bytes())
        return json.loads(payload)
    except InvalidToken:
        return None


def has_geotab_credentials() -> bool:
    return CREDENTIALS_FILE.exists()


def load_settings() -> dict:
    defaults = {"sync_interval_minutes": DEFAULT_SYNC_INTERVAL_MINUTES}
    if not SETTINGS_FILE.exists():
        return defaults
    try:
        defaults.update(json.loads(SETTINGS_FILE.read_text(encoding="utf-8")))
    except Exception:
        pass
    return defaults


def save_settings(settings: dict):
    SETTINGS_FILE.write_text(json.dumps(settings, ensure_ascii=False, indent=2), encoding="utf-8")


def get_session_secret() -> str:
    if SESSION_SECRET_FILE.exists():
        return SESSION_SECRET_FILE.read_text(encoding="utf-8")
    secret = secrets.token_hex(32)
    SESSION_SECRET_FILE.write_text(secret, encoding="utf-8")
    return secret
