#!/usr/bin/env python3
"""
audit_atomicat.py
------------------
Ferramenta de auditoria/limpeza para zips exportados do Atomicat, antes de
subir a pagina pro cPanel.

O QUE ESSE SCRIPT FAZ (por padrao):
  1. Corrige links de Termos de Uso / Politica de Privacidade no rodape que
     ainda apontam pra um dominio antigo/clonado (ex: go-xarion.site) —
     troca pelo dominio real da pagina.
  2. Baixa todas as imagens que ainda estao no CDN da Atomicat
     (media.atomicatmedia.net) e reescreve os caminhos pra uma pasta local
     (assets/ por padrao), assim a pagina fica independente do CDN.
  3. Neutraliza o beacon de telemetria de erro do player (VTurb) que manda
     dados do visitante (referrer, user-agent, idioma, nome interno da
     pagina) pra apido.atomicat-api.com. Isso e so remocao de uma chamada de
     rede que vaza fbclid/parametros de clique pra terceiro sem necessidade
     — nao afeta o funcionamento da pagina em si.
  4. Injeta a funcao ReadCookie() quando ela e usada (normalmente pelo pixel
     de postback da BuyGoods) mas nao esta definida no HTML exportado —
     e por isso que o pixel de conversao nao dispara.
  5. (Opcional, --legal-zip) Mescla um zip com as paginas reais de
     Terms of Use / Privacy Policy na raiz do export, nas pastas
     terms_of_use/ e policy_privacy/ — assim o link do rodape (corrigido
     no passo 1) aponta pra uma pagina que existe de fato, e nao só pro
     dominio certo.

O QUE ESSE SCRIPT **NAO** FAZ, DE PROPOSITO:
  - Nao toca no script de validacao de licenca da Atomicat (o bloco
    ofuscado que confere se sua assinatura esta ativa). Remover isso seria
    burlar o controle de licenciamento da ferramenta que voce paga pra
    usar, e isso eu nao vou automatizar. Se seu objetivo e nao depender
    mais da assinatura da Atomicat, o caminho correto e reconstruir a
    pagina fora do builder (ou falar com o suporte da Atomicat sobre um
    plano de export permanente), nao remover a checagem.

USO:
    python3 audit_atomicat.py entrada.zip saida.zip --domain seudominio.com

    Opcoes:
      --domain          dominio real da pagina (obrigatorio p/ corrigir rodape)
      --old-domain      dominio antigo a substituir (default: go-xarion.site)
      --assets-dir      nome da pasta de assets dentro do zip (default: assets)
      --cdn-host        host do CDN da Atomicat (default: media.atomicatmedia.net)
      --keep-telemetry  nao remove o beacon de telemetria (default: remove)
      --legal-zip       zip com as pastas terms_of_use/ e policy_privacy/
                         reais, pra mesclar na raiz do export
      --report-only     so gera o relatorio, nao escreve o zip de saida
"""

import argparse
import hashlib
import os
import re
import shutil
import sys
import tempfile
import urllib.request
import zipfile
from pathlib import Path

HTML_LIKE_EXT = {".html", ".htm"}
TEXT_SCAN_EXT = {".html", ".htm", ".css", ".js"}
IMAGE_EXT_PATTERN = r"[\w\-./%]+?\.(?:png|jpe?g|webp|gif|svg|ico|avif)"

READ_COOKIE_JS = """
function ReadCookie(name) {
  var nameEQ = name + "=";
  var ca = document.cookie.split(';');
  for (var i = 0; i < ca.length; i++) {
    var c = ca[i];
    while (c.charAt(0) === ' ') c = c.substring(1, c.length);
    if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
  }
  return null;
}
""".strip()


class Report:
    def __init__(self):
        self.legal_links_fixed = 0
        self.images_downloaded = 0
        self.images_failed = []
        self.telemetry_removed = 0
        self.readcookie_injected = False
        self.license_script_left_alone = False
        self.legal_pages_merged = []

    def summary(self):
        lines = ["=== Relatorio da auditoria ==="]
        lines.append(f"Links de Termos/Privacidade corrigidos: {self.legal_links_fixed}")
        lines.append(f"Imagens baixadas e localizadas: {self.images_downloaded}")
        if self.images_failed:
            lines.append(f"Imagens que FALHARAM ao baixar ({len(self.images_failed)}):")
            for url, err in self.images_failed:
                lines.append(f"   - {url} -> {err}")
        lines.append(f"Chamadas de telemetria removidas: {self.telemetry_removed}")
        lines.append(f"ReadCookie() injetado: {'sim' if self.readcookie_injected else 'nao foi preciso'}")
        if self.legal_pages_merged:
            lines.append(f"Paginas legais mescladas na raiz: {', '.join(self.legal_pages_merged)}")
        if self.license_script_left_alone:
            lines.append("Script de validacao de licenca da Atomicat: ENCONTRADO, mas NAO alterado (proposital).")
        return "\n".join(lines)


def extract_zip(zip_path: Path, dest_dir: Path):
    with zipfile.ZipFile(zip_path) as zf:
        zf.extractall(dest_dir)


def rezip(src_dir: Path, out_zip: Path):
    if out_zip.exists():
        out_zip.unlink()
    with zipfile.ZipFile(out_zip, "w", zipfile.ZIP_DEFLATED) as zf:
        for root, _, files in os.walk(src_dir):
            for f in files:
                full = Path(root) / f
                rel = full.relative_to(src_dir)
                zf.write(full, rel)


def iter_text_files(root: Path):
    for path in root.rglob("*"):
        if path.is_file() and path.suffix.lower() in TEXT_SCAN_EXT:
            yield path


def fix_legal_links(root: Path, old_domain: str, new_domain: str, report: Report):
    pattern = re.compile(
        r"https?://(?:www\.)?" + re.escape(old_domain) + r"(/[^\s\"'<>)]*)"
    )
    for path in iter_text_files(root):
        text = path.read_text(encoding="utf-8", errors="ignore")

        def _sub(m):
            report.legal_links_fixed += 1
            return f"https://{new_domain}{m.group(1)}"

        new_text, n = pattern.subn(_sub, text)
        if n:
            path.write_text(new_text, encoding="utf-8")


def _hash_name(url: str) -> str:
    ext = Path(url.split("?")[0]).suffix or ".bin"
    h = hashlib.sha1(url.encode()).hexdigest()[:10]
    base = Path(url.split("?")[0]).stem
    safe_base = re.sub(r"[^\w\-]", "_", base)[:40] or "img"
    return f"{safe_base}_{h}{ext}"


def download_images(root: Path, cdn_host: str, assets_dir_name: str, report: Report,
                     downloader=None):
    """downloader(url) -> bytes ; injectable for testing without real network."""
    downloader = downloader or _default_downloader
    assets_dir = root / assets_dir_name
    url_pattern = re.compile(
        r"https?://" + re.escape(cdn_host) + r"/" + IMAGE_EXT_PATTERN, re.IGNORECASE
    )
    url_to_local = {}

    # Pass 1: find every unique URL across all text files.
    all_files = list(iter_text_files(root))
    urls = set()
    for path in all_files:
        text = path.read_text(encoding="utf-8", errors="ignore")
        for m in url_pattern.finditer(text):
            urls.add(m.group(0))

    if not urls:
        return

    assets_dir.mkdir(parents=True, exist_ok=True)

    for url in sorted(urls):
        local_name = _hash_name(url)
        local_path = assets_dir / local_name
        try:
            data = downloader(url)
            local_path.write_bytes(data)
            url_to_local[url] = f"{assets_dir_name}/{local_name}"
            report.images_downloaded += 1
        except Exception as e:  # noqa: BLE001
            report.images_failed.append((url, str(e)))

    if not url_to_local:
        return

    # Pass 2: rewrite references.
    for path in all_files:
        text = path.read_text(encoding="utf-8", errors="ignore")
        original = text
        for url, local in url_to_local.items():
            text = text.replace(url, local)
        if text != original:
            path.write_text(text, encoding="utf-8")


def _default_downloader(url: str) -> bytes:
    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    with urllib.request.urlopen(req, timeout=20) as resp:
        return resp.read()


def remove_telemetry(root: Path, telemetry_host: str, report: Report):
    """Best-effort: comenta qualquer linha de JS que referencie o host de
    telemetria. E deliberadamente conservador (linha a linha) porque nao
    temos uma amostra real do bloco ofuscado; revise o diff antes de subir
    se a pagina tiver comportamento inesperado no player de video."""
    pattern = re.compile(re.escape(telemetry_host))
    for path in root.rglob("*.js"):
        text = path.read_text(encoding="utf-8", errors="ignore")
        if not pattern.search(text):
            continue
        new_lines = []
        changed = False
        for line in text.splitlines(keepends=True):
            if pattern.search(line):
                stripped = line.strip()
                new_lines.append(f"// [auditoria] linha removida (telemetria p/ {telemetry_host}): {stripped[:120]}\n")
                changed = True
                report.telemetry_removed += 1
            else:
                new_lines.append(line)
        if changed:
            path.write_text("".join(new_lines), encoding="utf-8")

    # Also handle inline <script> blocks inside HTML files.
    for path in root.rglob("*.html"):
        text = path.read_text(encoding="utf-8", errors="ignore")
        if not pattern.search(text):
            continue

        def _clean_script(m):
            block = m.group(0)
            if not pattern.search(block):
                return block
            lines = block.splitlines(keepends=True)
            out = []
            for line in lines:
                if pattern.search(line):
                    out.append(f"// [auditoria] linha removida (telemetria p/ {telemetry_host})\n")
                    report.telemetry_removed += 1
                else:
                    out.append(line)
            return "".join(out)

        new_text = re.sub(r"<script\b[^>]*>.*?</script>", _clean_script, text, flags=re.DOTALL | re.IGNORECASE)
        if new_text != text:
            path.write_text(new_text, encoding="utf-8")


def ensure_readcookie(root: Path, report: Report):
    for path in root.rglob("*.html"):
        text = path.read_text(encoding="utf-8", errors="ignore")
        uses_it = re.search(r"\bReadCookie\s*\(", text)
        defines_it = re.search(r"function\s+ReadCookie\s*\(", text)
        if uses_it and not defines_it:
            injection = f"<script>\n{READ_COOKIE_JS}\n</script>\n"
            if "<head>" in text:
                text = text.replace("<head>", "<head>\n" + injection, 1)
            else:
                text = injection + text
            path.write_text(text, encoding="utf-8")
            report.readcookie_injected = True


def merge_legal_pages(root: Path, legal_zip: Path, report: Report):
    """Extrai o zip de paginas legais e copia as pastas (ex: terms_of_use/,
    policy_privacy/) pra raiz do export, sobrescrevendo se ja existir algo
    com o mesmo nome."""
    with tempfile.TemporaryDirectory() as tmp:
        tmp_path = Path(tmp)
        extract_zip(legal_zip, tmp_path)
        for item in tmp_path.iterdir():
            if item.name.startswith("__MACOSX") or item.name == ".DS_Store":
                continue
            dest = root / item.name
            if item.is_dir():
                shutil.copytree(item, dest, dirs_exist_ok=True)
            else:
                dest.parent.mkdir(parents=True, exist_ok=True)
                shutil.copy2(item, dest)
            report.legal_pages_merged.append(item.name)


def check_license_script(root: Path, report: Report):
    """Apenas detecta e reporta — nunca remove/alterar de proposito."""
    suspicious = re.compile(r"(license|licenca|atomicat).{0,200}?(validate|check|valid)", re.IGNORECASE | re.DOTALL)
    for path in root.rglob("*.html"):
        text = path.read_text(encoding="utf-8", errors="ignore")
        if suspicious.search(text):
            report.license_script_left_alone = True
            return


def run(input_zip: str, output_zip: str, domain: str, old_domain: str,
        assets_dir: str, cdn_host: str, telemetry_host: str,
        keep_telemetry: bool, report_only: bool, downloader=None,
        legal_zip: str = None) -> Report:
    report = Report()
    with tempfile.TemporaryDirectory() as tmp:
        tmp_path = Path(tmp)
        extract_zip(Path(input_zip), tmp_path)

        if domain:
            fix_legal_links(tmp_path, old_domain, domain, report)

        download_images(tmp_path, cdn_host, assets_dir, report, downloader=downloader)

        if not keep_telemetry:
            remove_telemetry(tmp_path, telemetry_host, report)

        ensure_readcookie(tmp_path, report)
        check_license_script(tmp_path, report)

        if legal_zip:
            merge_legal_pages(tmp_path, Path(legal_zip), report)

        if not report_only:
            rezip(tmp_path, Path(output_zip))

    return report


def main():
    ap = argparse.ArgumentParser(description="Audita e limpa um zip exportado do Atomicat.")
    ap.add_argument("input_zip")
    ap.add_argument("output_zip")
    ap.add_argument("--domain", help="dominio real da pagina, ex: go-innerglow.site")
    ap.add_argument("--old-domain", default="go-xarion.site")
    ap.add_argument("--assets-dir", default="assets")
    ap.add_argument("--cdn-host", default="media.atomicatmedia.net")
    ap.add_argument("--telemetry-host", default="apido.atomicat-api.com")
    ap.add_argument("--keep-telemetry", action="store_true")
    ap.add_argument("--legal-zip", help="zip com terms_of_use/ e policy_privacy/ reais pra mesclar na raiz")
    ap.add_argument("--report-only", action="store_true")
    args = ap.parse_args()

    if not args.domain:
        print("Aviso: --domain nao foi passado, os links de Termos/Privacidade nao serao corrigidos.", file=sys.stderr)

    report = run(
        input_zip=args.input_zip,
        output_zip=args.output_zip,
        domain=args.domain,
        old_domain=args.old_domain,
        assets_dir=args.assets_dir,
        cdn_host=args.cdn_host,
        telemetry_host=args.telemetry_host,
        keep_telemetry=args.keep_telemetry,
        report_only=args.report_only,
        legal_zip=args.legal_zip,
    )
    print(report.summary())
    if not args.report_only:
        print(f"\nZip pronto: {args.output_zip}")


if __name__ == "__main__":
    main()
