#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Leads Per Hour — verificar-id.py · v2026.2
Auditor de conformidade com a identidade oficial (skill id-leadsperhour).

Varre HTML/CSS/JS/JSX/TSX/Vue/Svelte/SVG e acusa violações da marca:

  COR       [ERRO]   cor hex fora da paleta oficial (use var(--...) / tokens)
  FONTE     [ERRO]   font-family fora de Switzer/Inter (ou var(--font*))
  FONTE-CDN [ERRO]   <link> de fonte externa que não seja Inter/Switzer
  MICRO     [AVISO]  font-size menor que 10px (mínimo da interface é 11px)
  PRIMARIO  [AVISO]  mais de um .btn-primary no mesmo arquivo (um por vista)
  EMOJI     [AVISO]  emoji em arquivo de interface (proibido na UI)

USO
  python3 verificar-id.py <arquivo-ou-pasta> [...]
  python3 verificar-id.py src/ --strict          # avisos também reprovam
  python3 verificar-id.py . --ignore node_modules --ignore .test.

SAÍDA
  Lista arquivo:linha por violação + resumo. Exit code 1 se houver ERRO
  (ou qualquer achado com --strict) — pronto para CI/pre-commit.

LIMITAÇÕES CONHECIDAS
  - Cores em rgb()/hsl() literais não são checadas (use hex ou var()).
  - font-family quebrado em múltiplas linhas é coberto; outras declarações
    multilinha não.
  - Hex com alpha (#rrggbbaa/#rgba) é checado pelos 6 dígitos RGB.
"""
import re, sys, os

# ---------------------------------------------------------------------------
# PALETA PERMITIDA (hex RGB de 6 dígitos, minúsculo). Fontes da verdade:
# assets/tokens.css + réguas oficiais de tons e sombras + cores de sistema do
# design system + tinta oficial dos logos on-light (#181818) + logos de
# terceiros (Google/Microsoft/canais).
# ---------------------------------------------------------------------------
ALLOWED_HEX = set("""
f2552c d8321a 1d262d 203e49 115e66 131313 ffffff 000000 181818
141b1f 1a2229 26313a 0f1417 f4f7f8 9aa7ad 6d7a81 7a868d
12181c 10161a 0c1114 2a353d 38454f 2c3742 333f47 6b767c
f57756 da4d28 c24423 f2703d ffd9c4 e06c3b e05e3b e04131
2a8a91 2fbf71 f5a623 25d366 3b82c4 0a66c2 4a9ba6
fcfcfd f2f4f5 e9edee edf0f1 c9d1d4 5c6b73 7c878d 8e999f
c22d15 14834a a8690a 1da851 0f6a72 29717d 0f555c
e3e7e9 55636b f4f6f7
a93b1f 91331a 792b16 612212 49190d 301109 180804
f36641 f6886b f79980 f9aa96 fabbab fbccc0 fcddd5 feeeea
c22d17 ad2815 972312 821e10 6c190d 56140a 410f08 2b0a05 160503
dc4731 e05b48 e4705f e88476 ec998d efada3 f3c2ba f7d6d1 fbebe8
171e24 11171b 0f1317 0c0f12 090b0d 060809 030404
343c42 4a5157 61676c 777d81 8e9396 a5a8ab bbbec0 d2d4d5 e8e9ea
1d3842 1a323a 162b33 13252c 101f25 0d191d 0a1316 060c0f 030607
36515b 4d656d 637880 798b92 909fa4 a6b2b6 bcc5c8 d2d8db e9eced
0e4b52 0c4247 0a383d 092f33 072629 051c1f 031314 02090a
296e75 417e85 588e94 709ea3 88afb3 a0bfc2 b8cfd1 cfdfe0 e7eff0
111111 0f0f0f 0d0d0d 0b0b0b 0a0a0a 080808 060606 040404 020202
2b2b2b 424242 5a5a5a 717171 898989 a1a1a1 b8b8b8 d0d0d0 e7e7e7
e6e6e6 cccccc b3b3b3 999999 808080 666666 4c4c4c 333333 191919
4285f4 34a853 fbbc05 ea4335 f25022 7fba00 00a4ef ffb900
""".split())

ALLOWED_FONT_FIRST = {
    "switzer", "inter", "ui-monospace", "sfmono-regular", "menlo",
    "monospace", "inherit",
}
EMAIL_FONT_FIRST = {"helvetica", "arial"}  # permitido só em e-mail (aviso)

TEXT_EXT = {".html", ".htm", ".css", ".scss", ".less", ".js", ".jsx",
            ".ts", ".tsx", ".vue", ".svelte", ".astro", ".svg"}
SKIP_DIRS = {"node_modules", "dist", "build", ".git", ".next", ".nuxt",
             "coverage", "vendor", "out", ".svelte-kit"}

HEX_RE    = re.compile(r"#([0-9a-fA-F]{3,8})\b")
FF_RE     = re.compile(r"font-family\s*:\s*([^;}]+)", re.I)          # texto inteiro (multilinha)
GCDN_RE   = re.compile(r"fonts\.googleapis\.com/css2\?[^\"'\s>]*", re.I)
FAM_RE    = re.compile(r"family=([A-Za-z+%0-9]+)", re.I)
FSHARE_RE = re.compile(r"api\.fontshare\.com/[^\"'\s>]*f\[\]=([a-z-]+)", re.I)
FSIZE_RE  = re.compile(r"font-size\s*:\s*(\d+(?:\.\d+)?)px", re.I)
PRIM_RE   = re.compile(r"btn-primary")
DATAURI_RE= re.compile(r"data:[A-Za-z0-9+/;=.\-]*,[A-Za-z0-9+/=]*")
# emoji: pictogramas + símbolos usados como emoji em UI (não pega ✓ ✗ → ⌘)
EMOJI_RE  = re.compile("[\U0001F300-\U0001FAFF✅❌❎❗❓"
                       "⭐⭕⚠☀☔☕⚡❤️]")

def norm_hex(h):
    """Normaliza p/ 6 dígitos RGB (descarta alpha de #rgba/#rrggbbaa)."""
    h = h.lower()
    if len(h) == 3:
        return "".join(c * 2 for c in h)
    if len(h) == 4:
        return "".join(c * 2 for c in h[:3])
    if len(h) == 6:
        return h
    if len(h) == 8:
        return h[:6]
    return None  # 5/7 dígitos: id/valor não-cor

def line_of(text, pos):
    return text.count("\n", 0, pos) + 1

def scan_file(path, findings):
    try:
        text = open(path, encoding="utf-8", errors="replace").read()
    except OSError:
        return
    ext = os.path.splitext(path)[1].lower()
    is_email = "email" in os.path.basename(path).lower()
    is_ui = ext in {".html", ".htm", ".jsx", ".tsx", ".vue", ".svelte"}

    # ---- FONTE (texto inteiro: cobre declaração multilinha) ----
    for m in FF_RE.finditer(text):
        fam = m.group(1).strip()
        n = line_of(text, m.start())
        fam = re.sub(r"\s*!important\s*$", "", fam, flags=re.I)
        if fam.startswith("var("):
            continue
        first = fam.split(",")[0].strip().strip("'\"").strip().lower()
        if not first or first.startswith("var("):
            continue
        if first in ALLOWED_FONT_FIRST:
            continue
        if first in EMAIL_FONT_FIRST:
            if not is_email:
                findings.append((path, n, "AVISO", "FONTE",
                    f"font-family '{first}' — permitido apenas em template de e-mail"))
        else:
            findings.append((path, n, "ERRO", "FONTE",
                f"font-family '{first}' fora do padrão — títulos=Switzer, corpo=Inter (use var(--font-display)/var(--font))"))

    prim_hits = []
    for n, raw in enumerate(text.split("\n"), 1):
        # remove data-URIs (fontes/imagens embutidas) mas mantém o resto da linha
        line = DATAURI_RE.sub("", raw) if "data:" in raw else raw
        low = line.lower()
        stripped = line.lstrip()
        commented = stripped.startswith(("<!--", "//", "/*", "*"))

        # ---- COR ----
        for m in HEX_RE.finditer(line):
            h = norm_hex(m.group(1))
            if h is None or h in ALLOWED_HEX:
                continue
            before = line[max(0, m.start() - 8):m.start()]
            if 'href="' in before or "href='" in before or "url(" in before:
                continue  # âncora/fragmento/ref SVG, não cor
            if re.match(r"\s*[,{]", line[m.end():]):
                continue  # seletor CSS de id hex-like (#bad { … })
            findings.append((path, n, "ERRO", "COR",
                f"cor #{m.group(1)} fora da paleta — use os tokens (var(--...)) ou um hex oficial"))

        # ---- FONTE-CDN ----
        for um in GCDN_RE.finditer(line):
            for fm in FAM_RE.finditer(um.group(0)):
                fam = fm.group(1).split(":")[0].replace("+", " ").lower()
                if fam != "inter":
                    findings.append((path, n, "ERRO", "FONTE-CDN",
                        f"Google Fonts carregando '{fam}' — só Inter é permitida via Google"))
        for m in FSHARE_RE.finditer(line):
            if m.group(1).split("@")[0] != "switzer":
                findings.append((path, n, "ERRO", "FONTE-CDN",
                    f"Fontshare carregando '{m.group(1)}' — só Switzer é permitida via Fontshare"))

        # ---- MICRO ----
        for m in FSIZE_RE.finditer(line):
            try:
                px = float(m.group(1))
            except ValueError:
                continue
            if px < 10 and px > 1:  # 1px/0px = técnica de preheader/oculto
                findings.append((path, n, "AVISO", "MICRO",
                    f"font-size {m.group(1)}px — o mínimo da interface é 11px (10px só em rótulos-eyebrow)"))

        # ---- PRIMARIO (ignora linhas comentadas) ----
        if not commented and PRIM_RE.search(line) and ("class=" in low or "classname=" in low):
            prim_hits.append(n)

        # ---- EMOJI ----
        if is_ui and EMOJI_RE.search(line):
            findings.append((path, n, "AVISO", "EMOJI",
                "emoji em interface — proibido na UI (use ícones de traço)"))

    if len(prim_hits) > 1:
        findings.append((path, prim_hits[1], "AVISO", "PRIMARIO",
            f"{len(prim_hits)} ocorrências de btn-primary — a regra é UM primário por vista "
            f"(ok se o arquivo tiver várias telas/seções; linhas: {', '.join(map(str, prim_hits))})"))

def collect(paths, ignores):
    files = []
    for p in paths:
        if os.path.isfile(p):
            files.append(p)
        elif os.path.isdir(p):
            for root, dirs, names in os.walk(p):
                dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
                for name in names:
                    if os.path.splitext(name)[1].lower() in TEXT_EXT:
                        files.append(os.path.join(root, name))
    return [f for f in files if not any(ig in f for ig in ignores)]

def main(argv):
    paths, ignores, strict = [], [], False
    i = 0
    while i < len(argv):
        a = argv[i]
        if a == "--strict":
            strict = True
        elif a == "--ignore" and i + 1 < len(argv):
            ignores.append(argv[i + 1]); i += 1
        elif not a.startswith("--"):
            paths.append(a)
        i += 1
    if not paths:
        print(__doc__)
        return 2
    files = collect(paths, ignores)
    if not files:
        print("Nenhum arquivo elegível encontrado.")
        return 2
    findings = []
    for f in files:
        scan_file(f, findings)
    errs = [f for f in findings if f[2] == "ERRO"]
    warns = [f for f in findings if f[2] == "AVISO"]
    if findings:
        cur = None
        for path, n, sev, rule, msg in sorted(findings):
            if path != cur:
                print(f"\n/ {path}")
                cur = path
            mark = "✗" if sev == "ERRO" else "!"
            print(f"  {mark} L{n:<5} [{rule}] {msg}")
    print(f"\n{'—' * 60}")
    print(f"Arquivos verificados: {len(files)}  ·  ERROS: {len(errs)}  ·  AVISOS: {len(warns)}")
    if not findings:
        print("✓ Identidade 100% conforme. Somos a velocidade.")
    elif errs:
        print("✗ Corrija os ERROS antes de entregar (regras no SKILL.md da id-leadsperhour).")
    elif strict:
        print("✗ --strict: avisos reprovam — revise os itens acima.")
    else:
        print("✓ Sem erros — revise os avisos acima.")
    return 1 if (errs or (strict and findings)) else 0

if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
