import click
import httpx
import asyncio
import os
import sys
import subprocess
from datetime import datetime
from dotenv import load_dotenv
from supabase import create_client

load_dotenv()

# ─── CONFIG ───────────────────────────────────────────────────────────────────

SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
VT_KEY       = os.getenv("VT_API_KEY1")

supabase = create_client(SUPABASE_URL, SUPABASE_KEY) if SUPABASE_URL and SUPABASE_KEY else None

# ─── UI ───────────────────────────────────────────────────────────────────────

RED    = dict(fg="red",    bold=True)
WHITE  = dict(fg="white")
GREEN  = dict(fg="green",  bold=True)
YELLOW = dict(fg="yellow", bold=True)
BLUE   = dict(fg="cyan",   bold=True)
GREY   = dict(fg="bright_black")

def banner():
    click.echo()
    click.echo(click.style("  ╔══════════════════════════════════════╗", fg="red"))
    click.echo(click.style("  ║  ", fg="red") +
               click.style("🛡️  NpmProtect", fg="red", bold=True) +
               click.style("  ·  Vynex Labs        ", fg="white") +
               click.style("║", fg="red"))
    click.echo(click.style("  ║  ", fg="red") +
               click.style("npmprotect.vercel.app", fg="bright_black") +
               click.style("                 ║", fg="red"))
    click.echo(click.style("  ╚══════════════════════════════════════╝", fg="red"))
    click.echo()

def ok(msg):    click.echo(click.style("  ✔ ", **GREEN) + msg)
def warn(msg):  click.echo(click.style("  ⚠ ", **YELLOW) + msg)
def err(msg):   click.echo(click.style("  ✘ ", fg="red", bold=True) + msg)
def info(msg):  click.echo(click.style("  › ", **BLUE) + msg)
def sep():      click.echo(click.style("  ────────────────────────────────────────", fg="red"))

def badge(label, value, color="white"):
    click.echo(
        click.style(f"  {label:<14}", **RED) +
        click.style(str(value), fg=color, bold=True)
    )

def format_date(iso):
    try:   return datetime.fromisoformat(iso).strftime("%d/%m/%Y %H:%M")
    except: return iso

def score_color(mal, total):
    pct = mal / total if total else 0
    if pct == 0:   return "green"
    if pct < 0.1:  return "yellow"
    return "red"

# ─── HELPERS ──────────────────────────────────────────────────────────────────

async def check_npm_registry(package):
    try:
        async with httpx.AsyncClient(timeout=10.0) as c:
            r = await c.get(f"https://registry.npmjs.org/{package}")
            if r.status_code == 200:
                d = r.json()
                latest = d.get("dist-tags", {}).get("latest", "?")
                v = d.get("versions", {}).get(latest, {})
                return {
                    "exists": True,
                    "name": d.get("name"),
                    "version": latest,
                    "description": d.get("description", "—"),
                    "author": v.get("_npmUser", {}).get("name", "Desconhecido"),
                    "created": d.get("time", {}).get("created", ""),
                    "modified": d.get("time", {}).get("modified", ""),
                }
            return {"exists": False}
    except Exception as e:
        return {"exists": False, "error": str(e)}

async def check_npm_downloads(package):
    try:
        async with httpx.AsyncClient(timeout=10.0) as c:
            r = await c.get(f"https://api.npmjs.org/downloads/point/last-week/{package}")
            if r.status_code == 200:
                return r.json().get("downloads", 0)
    except: pass
    return 0

async def check_vt_package(package):
    if not VT_KEY: return None
    try:
        import base64
        url = f"https://www.npmjs.com/package/{package}"
        uid = base64.urlsafe_b64encode(url.encode()).rstrip(b"=").decode()
        async with httpx.AsyncClient(timeout=15.0) as c:
            r = await c.get(
                f"https://www.virustotal.com/api/v3/urls/{uid}",
                headers={"x-apikey": VT_KEY}
            )
            if r.status_code == 200:
                s = r.json()["data"]["attributes"]["last_analysis_stats"]
                return {k: s.get(k, 0) for k in ("malicious","suspicious","harmless","undetected")}
    except: pass
    return None

def _levenshtein(a, b):
    if len(a) < len(b): a, b = b, a
    if not b: return len(a)
    prev = list(range(len(b)+1))
    for ca in a:
        curr = [prev[0]+1]
        for j, cb in enumerate(b):
            curr.append(min(prev[j+1]+1, curr[j]+1, prev[j]+(ca!=cb)))
        prev = curr
    return prev[-1]

async def check_typosquat(package):
    popular = [
        "express","lodash","react","vue","axios","moment","chalk","webpack",
        "babel","typescript","eslint","prettier","jest","mocha","mongoose",
        "sequelize","dotenv","cors","helmet","socket.io","next","nuxt",
        "gatsby","vite","rollup","nodemon","pm2","commander","inquirer",
        "yargs","uuid",
    ]
    pkg = package.lower()
    return [p for p in popular if pkg != p and (pkg in p or p in pkg or _levenshtein(pkg, p) <= 2)][:3]

# ─── VALIDAÇÃO DE CHAVES ──────────────────────────────────────────────────────

async def validate_keys():
    """Valida todas as chaves do .env e retorna (ok, erros)."""
    errors = []
    results = {}

    # Supabase
    click.echo(click.style("  › ", **BLUE) + "Verificando Supabase...")
    try:
        sb = create_client(os.getenv("SUPABASE_URL",""), os.getenv("SUPABASE_SERVICE_ROLE",""))
        sb.table("reports").select("hash").limit(1).execute()
        results["supabase"] = True
        ok("Supabase SERVICE_ROLE    ✔")
    except Exception as e:
        results["supabase"] = False
        errors.append("SUPABASE_SERVICE_ROLE inválida ou SUPABASE_URL incorreta")
        err(f"Supabase                 ✘  ({str(e)[:50]})")

    # VirusTotal
    click.echo(click.style("  › ", **BLUE) + "Verificando VirusTotal...")
    vt1 = os.getenv("VT_API_KEY1","")
    if vt1:
        try:
            async with httpx.AsyncClient(timeout=10.0) as c:
                r = await c.get(
                    "https://www.virustotal.com/api/v3/files/275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f",
                    headers={"x-apikey": vt1}
                )
            if r.status_code in (200, 404):
                results["vt"] = True
                ok("VT_API_KEY1              ✔")
            else:
                results["vt"] = False
                errors.append("VT_API_KEY1 inválida")
                err(f"VT_API_KEY1              ✘  (status {r.status_code})")
        except Exception as e:
            results["vt"] = False
            errors.append("VT_API_KEY1 falhou")
            err(f"VT_API_KEY1              ✘  ({str(e)[:50]})")
    else:
        results["vt"] = False
        errors.append("VT_API_KEY1 não configurada")
        err("VT_API_KEY1              ✘  (não configurada)")

    # OpenRouter
    click.echo(click.style("  › ", **BLUE) + "Verificando OpenRouter...")
    or_key = os.getenv("OPENROUTER_API_KEY","")
    if or_key:
        try:
            async with httpx.AsyncClient(timeout=10.0) as c:
                r = await c.get(
                    "https://openrouter.ai/api/v1/models",
                    headers={"Authorization": f"Bearer {or_key}"}
                )
            if r.status_code == 200:
                results["openrouter"] = True
                ok("OPENROUTER_API_KEY       ✔")
            else:
                results["openrouter"] = False
                errors.append("OPENROUTER_API_KEY inválida")
                err(f"OPENROUTER_API_KEY       ✘  (status {r.status_code})")
        except Exception as e:
            results["openrouter"] = False
            errors.append("OpenRouter falhou")
            err(f"OPENROUTER_API_KEY       ✘  ({str(e)[:50]})")
    else:
        results["openrouter"] = False
        errors.append("OPENROUTER_API_KEY não configurada")
        err("OPENROUTER_API_KEY       ✘  (não configurada)")

    # Discord (opcional)
    dw = os.getenv("DISCORD_WEBHOOK","")
    if dw:
        results["discord"] = True
        ok("DISCORD_WEBHOOK          ✔")
    else:
        results["discord"] = False
        warn("DISCORD_WEBHOOK          — (opcional, notificações desativadas)")

    return results, errors

# ─── COMANDOS ─────────────────────────────────────────────────────────────────

@click.group()
def cli():
    """🛡️  NpmProtect — Threat Intelligence para o ecossistema npm.\n\n  Uso: np <comando> [opções]"""
    pass

# ── np check ──

@cli.command()
@click.argument("package")
@click.option("--vt", is_flag=True, help="Incluir análise do VirusTotal")
def check(package, vt):
    """Verifica se um pacote npm é seguro.\n\n  Exemplo: np check lodash\n  Exemplo: np check expresss --vt"""
    banner()
    info(f"Analisando pacote: {click.style(package, bold=True, fg='white')}\n")

    async def run():
        click.echo(click.style("  [1/3] ", **RED) + "Consultando npm registry...")
        npm = await check_npm_registry(package)

        if not npm["exists"]:
            click.echo()
            err(f"Pacote '{package}' não encontrado no npm registry.")
            typos = await check_typosquat(package)
            if typos:
                click.echo()
                warn("Possível typosquatting! Você quis dizer:")
                for t in typos:
                    click.echo(click.style(f"    → {t}", **YELLOW))
            click.echo()
            return

        click.echo()
        sep()
        badge("PACOTE",     npm["name"])
        badge("VERSÃO",     npm["version"])
        badge("DESCRIÇÃO",  npm["description"][:55])
        badge("AUTOR",      npm["author"])
        badge("CRIADO",     format_date(npm["created"]))
        badge("MODIFICADO", format_date(npm["modified"]))
        sep()

        click.echo(click.style("  [2/3] ", **RED) + "Verificando popularidade...")
        downloads = await check_npm_downloads(package)
        dl_color = "green" if downloads > 10000 else "yellow" if downloads > 100 else "red"
        badge("DOWNLOADS", f"{downloads:,} / semana", dl_color)
        if downloads < 100:
            warn("Poucos downloads — pacote pouco conhecido!")

        typos = await check_typosquat(package)
        if typos:
            click.echo()
            warn(f"Nome similar a: {', '.join(typos)}")
            warn("Verifique se não é um typosquat!")

        sep()
        click.echo(click.style("  [3/3] ", **RED) + "Verificando base NpmProtect...")

        db_resp = supabase.table("reports") \
            .select("hash, report_id, created_at") \
            .ilike("content", f"%{package}%") \
            .limit(3).execute() if supabase else None

        if db_resp and db_resp.data:
            click.echo()
            warn(f"{len(db_resp.data)} relatório(s) associado(s) a '{package}'!")
            for r in db_resp.data:
                click.echo(
                    click.style(f"    • {r['hash'][:32]}...", fg="red") +
                    click.style(f"  [{format_date(r['created_at'])}]", **GREY)
                )
        else:
            badge("BASE", "Nenhuma ameaça registrada.", "green")

        if vt:
            click.echo()
            click.echo(click.style("  [VT]  ", **RED) + "Consultando VirusTotal...")
            vt_r = await check_vt_package(package)
            if vt_r:
                mal, sus = vt_r["malicious"], vt_r["suspicious"]
                tot = sum(vt_r.values())
                badge("VT SCORE", f"{mal} malicioso(s) / {sus} suspeito(s) de {tot}", score_color(mal+sus, tot))
                if mal > 0:   err("VirusTotal detectou engines MALICIOSAS!")
                elif sus > 0: warn("VirusTotal detectou engines suspeitas.")
                else:         ok("VirusTotal: limpo.")
            else:
                info("VirusTotal: sem dados ou chave não configurada.")

        sep()
        click.echo()
        is_safe = downloads > 1000 and not (db_resp and db_resp.data) and not typos
        if is_safe: ok(f"'{package}' parece seguro para uso.")
        else:       warn(f"'{package}' tem pontos de atenção. Revise antes de instalar.")
        click.echo()

    asyncio.run(run())

# ── np report ──

@cli.command()
@click.argument("hash_value")
def report(hash_value):
    """Busca o relatório completo de um hash SHA-256.\n\n  Exemplo: np report abc123..."""
    banner()
    info(f"Buscando relatório: {click.style(hash_value[:20] + '...', bold=True)}")

    if not supabase:
        err("Supabase não configurado. Verifique seu .env")
        return

    try:
        resp = supabase.table("reports").select("*").eq("hash", hash_value).limit(1).execute()
        if resp.data:
            r = resp.data[0]
            click.echo()
            sep()
            badge("Hash",      r["hash"])
            badge("Report ID", r["report_id"])
            badge("Score",     str(r.get("score","—")) + "/100")
            badge("Analista",  r["analyst"])
            badge("Data",      format_date(r["created_at"]))
            sep()
            click.echo()
            for line in r["content"].splitlines()[:30]:
                click.echo("  " + line)
            if len(r["content"].splitlines()) > 30:
                click.echo()
                info("Relatório completo: https://npmprotect.vercel.app")
        else:
            warn("Nenhum relatório encontrado para esse hash.")
    except Exception as e:
        err(f"Erro: {e}")

# ── np latest ──

@cli.command()
@click.option("--limit", default=10, help="Número de resultados (padrão: 10)")
def latest(limit):
    """Lista os últimos malwares detectados.\n\n  Exemplo: np latest --limit 5"""
    banner()
    info(f"Últimos {limit} malwares detectados:\n")

    if not supabase:
        err("Supabase não configurado. Verifique seu .env")
        return

    try:
        resp = supabase.table("reports") \
            .select("hash, report_id, created_at, score") \
            .order("created_at", desc=True).limit(limit).execute()
        if resp.data:
            sep()
            for i, r in enumerate(resp.data, 1):
                score = r.get("score")
                if score is None:   s_str = ""
                elif score >= 80:   s_str = click.style(f"  [{score}/100 CRITICAL]", fg="red",    bold=True)
                elif score >= 60:   s_str = click.style(f"  [{score}/100 HIGH]",     fg="yellow",  bold=True)
                elif score >= 40:   s_str = click.style(f"  [{score}/100 MEDIUM]",   fg="yellow")
                else:               s_str = click.style(f"  [{score}/100 LOW]",      fg="green")
                click.echo(
                    click.style(f"  {i:02d}. ", **RED) +
                    click.style(r["hash"][:40] + "...", fg="white") +
                    s_str +
                    click.style(f"  {format_date(r['created_at'])}", **GREY)
                )
            sep()
            click.echo()
            info("Dashboard: https://npmprotect.vercel.app")
        else:
            warn("Nenhum relatório encontrado.")
    except Exception as e:
        err(f"Erro: {e}")

# ── np stats ──

@cli.command()
def stats():
    """Estatísticas gerais da base de inteligência."""
    banner()
    if not supabase:
        err("Supabase não configurado. Verifique seu .env")
        return
    try:
        total_resp = supabase.table("reports").select("created_at", count="exact").execute()
        total = total_resp.count or 0
        last_resp = supabase.table("reports").select("created_at") \
            .order("created_at", desc=True).limit(1).execute()
        last = format_date(last_resp.data[0]["created_at"]) if last_resp.data else "N/A"
        critical_resp = supabase.table("reports").select("hash", count="exact").gte("score", 80).execute()
        critical = critical_resp.count or 0
        sep()
        badge("Total",      total,  "red")
        badge("Críticos",   critical, "red")
        badge("Última",     last)
        badge("Dashboard",  "https://npmprotect.vercel.app")
        badge("Analista",   "Mozart_Dev · Vynex Labs")
        sep()
        click.echo()
    except Exception as e:
        err(f"Erro: {e}")

# ── np analisar ──

@cli.command()
@click.option("--force", is_flag=True, help="Rodar mesmo se alguma chave falhar")
def analisar(force):
    """Valida as chaves do .env e inicia o hunter automaticamente.\n\n  Exemplo: np analisar\n  Exemplo: np analisar --force"""
    banner()
    info("Iniciando verificação de ambiente...\n")

    async def run():
        sep()
        click.echo(click.style("  VALIDANDO CHAVES\n", **RED))
        results, errors = await validate_keys()
        sep()

        required_ok = results.get("supabase") and results.get("vt") and results.get("openrouter")

        if errors:
            click.echo()
            for e in errors:
                warn(e)

        click.echo()

        if not required_ok and not force:
            err("Chaves obrigatórias inválidas. Corrija o .env antes de continuar.")
            info("Use 'np analisar --force' para rodar mesmo assim.")
            click.echo()
            return

        if not required_ok and force:
            warn("Rodando com chaves inválidas (--force ativo).")
            click.echo()

        ok("Ambiente validado! Iniciando hunter...\n")

        # Localiza o hunter.py relativo ao script ou ao cwd
        script_dir = os.path.dirname(os.path.abspath(__file__))
        candidates = [
            os.path.join(script_dir, "..", "servidor", "hunter.py"),
            os.path.join(script_dir, "hunter.py"),
            os.path.join(os.getcwd(), "servidor", "hunter.py"),
            os.path.join(os.getcwd(), "hunter.py"),
        ]
        hunter_path = next((p for p in candidates if os.path.exists(p)), None)

        if not hunter_path:
            err("hunter.py não encontrado. Execute dentro do repositório NpmProtect.")
            return

        sep()
        info(f"Hunter: {hunter_path}")
        info("Pressione Ctrl+C para parar.\n")
        sep()
        click.echo()

        try:
            subprocess.run([sys.executable, hunter_path], check=True)
        except KeyboardInterrupt:
            click.echo()
            warn("Hunter interrompido pelo usuário.")
        except subprocess.CalledProcessError as e:
            err(f"Hunter encerrou com erro: {e}")

    asyncio.run(run())

# ─── ENTRY POINT ──────────────────────────────────────────────────────────────

if __name__ == "__main__":
    cli()