"""Licenciamento do DNS Shield ISP por contrato ativo no ERP IXC.

O painel do provedor **não** conversa com o IXC: ele consulta o serviço central
do desenvolvedor (``licenca-server``), enviando apenas CNPJ + chave de
instalação. O serviço central é quem guarda a URL e o token de leitura do IXC,
localiza o cliente pelo CNPJ e avalia o status do contrato.

A resposta vem assinada (HMAC-SHA256 com a chave de instalação) para que o
veredito não possa ser forjado apontando a URL para um servidor falso.

Módulos controlados: ``anablock`` e ``federacao``. Bloqueios e liberações
manuais nunca dependem de licença.

Desenvolvedor: Euquias Junior <junior@jbinfo.com.br>
"""
from __future__ import annotations

import hashlib
import hmac
import json
import sqlite3
from datetime import datetime, timedelta, timezone
from functools import wraps
from pathlib import Path

import requests
from flask import flash, redirect, url_for

from .config import APP_VERSION
from .db import get_db

DEFAULT_URL = "https://licenca.jbinfo.com.br"
ENDPOINT = "/api/licenca/verificar"
ASSINATURA_HEADER = "X-Licenca-Assinatura"
TIMEOUT = 25

MODULOS = ("anablock", "federacao")
MODULO_LABEL = {
    "anablock": "Integração AnaBlock",
    "federacao": "Federação entre provedores",
}

# Dias em que o último veredito positivo continua valendo se o serviço de
# licenças estiver inacessível (falha de rede, manutenção).
CARENCIA_DIAS = 7

STATUS_LABEL = {
    "ativo": "Contrato ativo",
    "inativo": "Contrato inativo ou cancelado",
    "financeiro": "Contrato em bloqueio financeiro",
    "sem_contrato": "Nenhum contrato do DNS Shield ISP localizado",
    "cnpj_nao_encontrado": "CNPJ não encontrado no cadastro do desenvolvedor",
    "nao_configurado": "Licenciamento ainda não configurado",
    "erro": "Falha ao consultar o serviço de licenças",
}

DEFAULTS = {
    "cnpj": "",
    "chave": "",
    "url": DEFAULT_URL,
    "status": "nao_configurado",
    "modulos": "",
    "contrato": "",
    "validade": "",
    "mensagem": "",
    "ultima_verificacao": None,
    "ultima_verificacao_ok": None,
}


class LicencaError(RuntimeError):
    """Módulo bloqueado por ausência de licença ativa."""


# --------------------------------------------------------------- persistência

def _url_padrao() -> str:
    try:
        from .config import get_config

        return (get_config().licenca_url or DEFAULT_URL).rstrip("/")
    except Exception:  # noqa: BLE001
        return DEFAULT_URL


def get_settings() -> dict:
    try:
        row = get_db().execute("SELECT * FROM licenca WHERE id = 1").fetchone()
    except sqlite3.Error:
        row = None
    if row is None:
        data = dict(DEFAULTS)
        data["url"] = _url_padrao()
        return data
    data = dict(DEFAULTS)
    data.update({k: v for k, v in dict(row).items() if k != "id"})
    data["url"] = (data.get("url") or _url_padrao()).rstrip("/")
    return data


def save_settings(*, cnpj: str, chave: str, url: str) -> None:
    atual = get_settings()
    db = get_db()
    db.execute(
        "INSERT INTO licenca (id, cnpj, chave, url) VALUES (1, ?, ?, ?) "
        "ON CONFLICT(id) DO UPDATE SET cnpj = excluded.cnpj, chave = excluded.chave, "
        "url = excluded.url",
        (
            somente_digitos(cnpj),
            chave.strip() or (atual.get("chave") or ""),
            (url.strip() or DEFAULT_URL).rstrip("/"),
        ),
    )
    db.commit()


def _record(
    *,
    status: str,
    modulos: list[str],
    contrato: str,
    validade: str,
    mensagem: str,
    ok: bool,
) -> None:
    db = get_db()
    if ok:
        db.execute(
            "UPDATE licenca SET status = ?, modulos = ?, contrato = ?, validade = ?, "
            "mensagem = ?, ultima_verificacao = datetime('now'), "
            "ultima_verificacao_ok = datetime('now') WHERE id = 1",
            (status, ",".join(modulos), contrato, validade, mensagem[:500]),
        )
    else:
        db.execute(
            "UPDATE licenca SET mensagem = ?, ultima_verificacao = datetime('now') WHERE id = 1",
            (mensagem[:500],),
        )
    db.commit()


def somente_digitos(valor: str) -> str:
    return "".join(ch for ch in (valor or "") if ch.isdigit())


def fingerprint() -> str:
    """Identificador estável e anônimo da instalação (machine-id + hostname)."""
    partes = []
    for caminho in ("/etc/machine-id", "/var/lib/dbus/machine-id"):
        try:
            partes.append(Path(caminho).read_text(encoding="utf-8").strip())
            break
        except OSError:
            continue
    try:
        partes.append(Path("/etc/hostname").read_text(encoding="utf-8").strip())
    except OSError:
        pass
    if not partes:
        return "desconhecido"
    return hashlib.sha256("|".join(partes).encode("utf-8")).hexdigest()[:32]


def _assinatura(chave: str, corpo: bytes) -> str:
    return hmac.new(chave.encode("utf-8"), corpo, hashlib.sha256).hexdigest()


# ------------------------------------------------------------------ verificação

def verificar() -> dict:
    """Consulta o serviço de licenças e grava o veredito. Nunca levanta exceção."""
    cfg = get_settings()
    cnpj = somente_digitos(cfg.get("cnpj") or "")
    chave = (cfg.get("chave") or "").strip()
    base = (cfg.get("url") or DEFAULT_URL).rstrip("/")

    if not cnpj or not chave:
        _record(
            status="nao_configurado",
            modulos=[],
            contrato="",
            validade="",
            mensagem="Informe o CNPJ e a chave de instalação em Configurações → Licenciamento.",
            ok=True,
        )
        return estado()

    payload = {
        "cnpj": cnpj,
        "chave": chave,
        "versao": APP_VERSION,
        "fingerprint": fingerprint(),
    }

    try:
        resp = requests.post(
            f"{base}{ENDPOINT}",
            json=payload,
            timeout=TIMEOUT,
            headers={"User-Agent": f"DNS-Shield-ISP/{APP_VERSION}"},
        )
    except requests.RequestException as exc:
        _record(status="", modulos=[], contrato="", validade="",
                mensagem=f"serviço de licenças inacessível: {exc}", ok=False)
        return estado()

    if resp.status_code in (401, 403):
        _record(
            status="inativo",
            modulos=[],
            contrato="",
            validade="",
            mensagem="Chave de instalação recusada pelo serviço de licenças.",
            ok=True,
        )
        return estado()
    if resp.status_code != 200:
        _record(status="", modulos=[], contrato="", validade="",
                mensagem=f"serviço de licenças respondeu HTTP {resp.status_code}", ok=False)
        return estado()

    corpo = resp.content
    esperado = _assinatura(chave, corpo)
    recebida = (resp.headers.get(ASSINATURA_HEADER) or "").strip()
    if not recebida or not hmac.compare_digest(esperado, recebida):
        _record(status="", modulos=[], contrato="", validade="",
                mensagem="resposta do serviço de licenças sem assinatura válida (ignorada)",
                ok=False)
        return estado()

    try:
        data = json.loads(corpo.decode("utf-8"))
    except (ValueError, UnicodeDecodeError):
        _record(status="", modulos=[], contrato="", validade="",
                mensagem="resposta do serviço de licenças em formato inválido", ok=False)
        return estado()

    status = str(data.get("status") or "erro")
    modulos = [m for m in (data.get("modulos") or []) if m in MODULOS]
    if status != "ativo":
        modulos = []
    _record(
        status=status,
        modulos=modulos,
        contrato=str(data.get("contrato") or ""),
        validade=str(data.get("validade") or ""),
        mensagem=str(data.get("mensagem") or STATUS_LABEL.get(status, status)),
        ok=True,
    )
    return estado()


def _dentro_carencia(cfg: dict) -> bool:
    raw = cfg.get("ultima_verificacao_ok")
    if not raw:
        return False
    try:
        parsed = datetime.strptime(str(raw), "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
    except (TypeError, ValueError):
        return False
    return datetime.now(timezone.utc) - parsed <= timedelta(days=CARENCIA_DIAS)


def estado() -> dict:
    """Situação efetiva da licença, já aplicando a carência de rede."""
    cfg = get_settings()
    status = cfg.get("status") or "nao_configurado"
    modulos = [m for m in (cfg.get("modulos") or "").split(",") if m]
    autorizado = status == "ativo" and _dentro_carencia(cfg)
    if status == "ativo" and not _dentro_carencia(cfg):
        motivo = (
            f"Licença não revalidada nos últimos {CARENCIA_DIAS} dias. "
            "Verifique a conexão com o serviço de licenças."
        )
    elif autorizado:
        motivo = cfg.get("mensagem") or STATUS_LABEL["ativo"]
    else:
        motivo = cfg.get("mensagem") or STATUS_LABEL.get(status, status)

    return {
        **cfg,
        "autorizado": autorizado,
        "modulos_ativos": modulos if autorizado else [],
        "status_label": STATUS_LABEL.get(status, status),
        "motivo": motivo,
        "carencia_dias": CARENCIA_DIAS,
        "configurado": bool(cfg.get("cnpj") and cfg.get("chave")),
    }


def modulo_ativo(modulo: str) -> bool:
    st = estado()
    return bool(st["autorizado"]) and modulo in st["modulos_ativos"]


def garantir_modulo(modulo: str) -> None:
    """Levanta LicencaError quando o módulo não está liberado (uso no backend)."""
    if not modulo_ativo(modulo):
        st = estado()
        raise LicencaError(
            f"{MODULO_LABEL.get(modulo, modulo)} não licenciado: {st['motivo']}"
        )


def requer_modulo(modulo: str, destino: str = "settings.index"):
    """Protege rotas do painel; devolve o usuário com um aviso claro."""

    def decorator(view):
        @wraps(view)
        def wrapper(*args, **kwargs):
            if not modulo_ativo(modulo):
                st = estado()
                flash(
                    f"{MODULO_LABEL.get(modulo, modulo)} está em modo demonstração: "
                    f"{st['motivo']}",
                    "error",
                )
                return redirect(url_for(destino))
            return view(*args, **kwargs)

        return wrapper

    return decorator
