#!/usr/bin/env python3
"""
CRIUS COMPRESSOR — DeepSeek Audit Runner via API
Memanggil DeepSeek API langsung untuk setiap prompt audit.
Tidak perlu opencode CLI.

Usage:
  python run_audit_api.py                         # semua kategori
  python run_audit_api.py security                # satu kategori
  python run_audit_api.py --start-from 5          # lanjut dari prompt ke-5
  python run_audit_api.py --dry-run               # lihat daftar tanpa eksekusi

API Key:
  - export DEEPSEEK_API_KEY="sk-..."    (Linux/Mac)
  - set DEEPSEEK_API_KEY=sk-...         (Windows CMD)
  - $env:DEEPSEEK_API_KEY="sk-..."      (Windows PowerShell)
"""

import sys
import json
import time
import argparse
import urllib.request
import urllib.error
import os
from datetime import datetime
from pathlib import Path

# ─── CONFIG ─────────────────────────────────────────────────────────────
API_URL         = "https://api.deepseek.com/chat/completions"
MODEL           = "deepseek-chat"       # atau "deepseek-reasoner"
OUTPUT_DIR      = Path("audit_results")
PROJECT_ROOT    = Path(".")
MAX_RETRIES     = 3
RETRY_DELAY     = 5                     # detik antar retry
RATE_LIMIT_WAIT = 10                    # detik antar prompt (hindari rate limit)
MAX_FILE_CHARS  = 8000                  # max karakter per file yg dikirim ke AI
# ────────────────────────────────────────────────────────────────────────

API_KEY = os.environ.get("DEEPSEEK_API_KEY") or os.environ.get("OPENAI_API_KEY")

CONTEXT_HEADER = """Kamu adalah expert Laravel security & UI auditor.
Project: CRIUS COMPRESSOR — Laravel 12, Tailwind CSS, Alpine.js, Bilingual ID/EN.
Stack: PHP 8.3, Laravel 12, SQLite, Tailwind CSS v3, Alpine.js v3.
Design system: Crius design tokens (navy, blue-light, accent), font Montserrat.
Admin layout: resources/views/layouts/admin.blade.php
Frontend layout: resources/views/layouts/app.blade.php
Helpers: setting(), locale_field(), active_locale() di app/Helpers/helpers.php
Bilingual: field _id / _en suffix, middleware SetLocale, URL prefix /en.

ATURAN OUTPUT KAMU:
1. Jika ada BUG atau PELANGGARAN STANDAR -> langsung tulis fix-nya (kode lengkap).
2. Format: [FILE PATH] -> [MASALAH] -> [FIX CODE].
3. Jika file sudah benar -> tulis "OK: [alasan]".
4. JANGAN penjelasan panjang. Langsung ke kode.
5. Prioritas: CRITICAL > HIGH > MEDIUM > LOW."""


# ═══════════════════════════════════════════════════════════════════════════
#  PROMPT BANK — sama persis dengan run_audit.py
# ═══════════════════════════════════════════════════════════════════════════

PROMPTS = {

  "security": [
    {
      "id": "SEC-01", "title": "CSRF Protection — Semua Form",
      "target_files": ["resources/views/frontend/contact/index.blade.php","resources/views/frontend/careers/show.blade.php","resources/views/admin/**/*.blade.php"],
      "prompt": """TASK: Audit CSRF protection di semua form Blade.
CEK: 1. @csrf di setiap form POST? 2. @method('PUT'/'DELETE') untuk non-POST? 3. form GET dengan data sensitif?
Cek file frontend contact, careers, dan SEMUA admin blade dengan <form>.
Tulis fix lengkap untuk setiap masalah."""
    },
    {
      "id": "SEC-02", "title": "Authentication & Authorization — Middleware Admin",
      "target_files": ["routes/admin.php","app/Http/Middleware/AdminAuth.php","app/Http/Controllers/Admin/DashboardController.php"],
      "prompt": """TASK: Audit autentikasi & otorisasi admin.
CEK: 1. Semua route admin dibungkus middleware? 2. AdminAuth.php redirect logic? 3. Ada route tanpa proteksi? 4. 403 vs 401 benar? 5. Session fixation? 6. Hardcoded credential?
Tampilkan semua route vulnerability + fix."""
    },
    {
      "id": "SEC-03", "title": "File Upload Security",
      "target_files": ["app/Services/MediaUploadService.php","app/Http/Controllers/Admin/MediaLibraryController.php","app/Http/Controllers/Admin/ProductController.php","app/Http/Controllers/Admin/BlogPostController.php","app/Http/Controllers/Frontend/CareerController.php"],
      "prompt": """TASK: Audit keamanan file upload.
CEK: 1. Validasi MIME (bukan hanya ekstensi)? 2. File di-rename random? 3. Size limit? 4. Path traversal? 5. Block ekstensi berbahaya?
Tulis fix lengkap untuk setiap upload handler."""
    },
    {
      "id": "SEC-04", "title": "SQL Injection & Mass Assignment",
      "target_files": ["app/Models/*.php","app/Http/Controllers/Admin/*.php","app/Http/Requests/Admin/*.php"],
      "prompt": """TASK: Audit SQL Injection dan Mass Assignment.
CEK: 1. $guarded = [] tanpa $fillable? 2. DB::raw() dengan user input? 3. $request->all() langsung ke create()? 4. N+1 query? 5. Form Request rules lengkap?
Tulis fix untuk setiap model dan controller."""
    },
    {
      "id": "SEC-05", "title": "XSS Prevention — Blade Output",
      "target_files": ["resources/views/frontend/**/*.blade.php","resources/views/admin/**/*.blade.php"],
      "prompt": """TASK: Audit XSS di semua Blade views.
CEK: 1. {!! !!} untuk user input? 2. {!! !!} hanya untuk rich-text trusted? 3. Inline JS dengan PHP variable tanpa json_encode?
Tulis fix untuk setiap masalah."""
    },
    {
      "id": "SEC-06", "title": "Rate Limiting & Contact Form Spam",
      "target_files": ["routes/web.php","app/Http/Controllers/Frontend/ContactController.php","app/Http/Controllers/Frontend/CareerController.php","app/Http/Requests/Frontend/ContactFormRequest.php","app/Http/Requests/Frontend/CareerApplyRequest.php"],
      "prompt": """TASK: Audit rate limiting dan anti-spam.
CEK: 1. throttle middleware di routes? 2. Validasi email, message, phone? 3. IP address tersimpan? 4. Duplicate check? 5. CV hanya PDF max 2MB?
Tulis fix lengkap."""
    },
    {
      "id": "SEC-07", "title": "Environment & Config Security",
      "target_files": [".env.example","config/crius.php","config/app.php","bootstrap/app.php"],
      "prompt": """TASK: Audit keamanan konfigurasi.
CEK: 1. .env.example lengkap tanpa nilai asli? 2. APP_DEBUG=false di production? 3. Hardcoded secret? 4. Session secure? 5. Cookie httponly?
Tulis fix lengkap."""
    },
    {
      "id": "SEC-08", "title": "Admin Media Library — Path Traversal & Deletion",
      "target_files": ["app/Http/Controllers/Admin/MediaLibraryController.php"],
      "prompt": """TASK: Audit path traversal dan unauthorized file deletion.
CEK: 1. Sanitasi filename? 2. Validasi path hanya di storage/public? 3. Auth check? 4. Symlink traversal?
Tulis method upload() dan delete() yang aman."""
    },
  ],

  "admin_ui": [
    {
      "id": "ADM-01", "title": "Admin Table — Responsif & Standar CSS Class",
      "target_files": ["resources/views/admin/products/index.blade.php","resources/views/admin/blog/posts/index.blade.php","resources/views/admin/careers/index.blade.php","resources/views/admin/contact-messages/index.blade.php","resources/views/admin/testimonials/index.blade.php"],
      "prompt": """TASK: Audit responsivitas tabel admin.
STANDAR: admin-table-card > admin-table-head > admin-table-wrap > table > thead/th > tbody/td.
Kolom action: class admin-table-actions. Badge: badge--blue / badge--inactive.
CEK setiap file: ada wrap? overflow? class action benar?
Tulis fix untuk setiap tabel."""
    },
    {
      "id": "ADM-02", "title": "Admin Form — Standar Input & Validasi Error Display",
      "target_files": ["resources/views/admin/products/create.blade.php","resources/views/admin/products/edit.blade.php","resources/views/admin/blog/posts/form.blade.php","resources/views/admin/services/form.blade.php","resources/views/admin/careers/form.blade.php"],
      "prompt": """TASK: Audit standarisasi form admin.
STANDAR: admin-form-card > admin-form-header > admin-form-body > admin-form-footer.
Setiap input: @error, old(), label. Bilingual: grid 2 kolom. Toggle: Alpine.js.
CEK setiap file. Tulis fix."""
    },
    {
      "id": "ADM-03", "title": "Admin Dashboard — Stats & Quick Actions",
      "target_files": ["resources/views/admin/dashboard/index.blade.php","app/Http/Controllers/Admin/DashboardController.php"],
      "prompt": """TASK: Audit dashboard admin.
CEK controller: stat dari DB (bukan hardcoded)? Query optimal?
CEK view: admin-stat-card class? Link ke inbox? Tabel pesan terbaru? Produk terbaru?
Tulis fix."""
    },
    {
      "id": "ADM-04", "title": "Admin Settings — CMS Groups",
      "target_files": ["resources/views/admin/settings/homepage.blade.php","resources/views/admin/settings/general.blade.php","resources/views/admin/settings/contact.blade.php","resources/views/admin/settings/seo.blade.php","resources/views/admin/settings/integrations.blade.php","app/Http/Controllers/Admin/SiteSettingController.php"],
      "prompt": """TASK: Audit halaman settings admin.
CEK controller: query by group? updateOrCreate? Image path?
CEK view: semua field ada? Preview gambar? GA4 helper text? Password input untuk API key?
Tulis fix."""
    },
    {
      "id": "ADM-05", "title": "Admin Sidebar — Active State & Mobile Toggle",
      "target_files": ["resources/views/layouts/partials/admin/sidebar.blade.php","resources/views/layouts/admin.blade.php"],
      "prompt": """TASK: Audit sidebar admin.
CEK: active state request()->routeIs()? Semua menu grup? Icon SVG? Mobile toggle? Overlay?
Tulis fix untuk semua masalah."""
    },
  ],

  "frontend_ux": [
    {
      "id": "FE-01", "title": "Homepage — Sections Completeness & CMS Integration",
      "target_files": ["resources/views/frontend/home/index.blade.php","app/Http/Controllers/Frontend/HomeController.php"],
      "prompt": """TASK: Audit homepage — 14 section dan integrasi CMS.
CEK controller: hero_slides, featuredProducts, categories, stats, testimonials, brands, latestPosts, industries.
CEK view: hero slider Alpine.js auto-play? Stats counter animasi? WA float scroll? Brand marquee?
Tulis fix untuk missing section."""
    },
    {
      "id": "FE-02", "title": "Product Detail — Tabs, Specs Table, Gallery",
      "target_files": ["resources/views/frontend/products/show.blade.php","app/Http/Controllers/Frontend/ProductController.php"],
      "prompt": """TASK: Audit halaman detail produk.
CEK controller: eager loading semua relasi? Specs digroup? Related products? WA link dinamis?
CEK view: gallery thumbnail click? Tabs Alpine.js? Specs table per group? Highlights metric?
Tulis fix."""
    },
    {
      "id": "FE-03", "title": "Contact Form — UX, Validation & Email",
      "target_files": ["resources/views/frontend/contact/index.blade.php","app/Http/Controllers/Frontend/ContactController.php","app/Http/Requests/Frontend/ContactFormRequest.php","app/Mail/ContactInquiryMail.php","app/Mail/ContactAutoReplyMail.php"],
      "prompt": """TASK: Audit halaman kontak.
CEK controller: simpan ke DB? Kirim email admin + auto-reply? Try-catch? IP tersimpan?
CEK request: rules lengkap? Pesan error bilingual? Honeypot?
CEK view: success alert? Error per-field? Dropdown produk dari DB? Loading state?
Tulis fix lengkap ContactController@send()."""
    },
    {
      "id": "FE-04", "title": "Blog — SEO, Pagination & Sidebar",
      "target_files": ["resources/views/frontend/blog/index.blade.php","resources/views/frontend/blog/show.blade.php","app/Http/Controllers/Frontend/BlogController.php"],
      "prompt": """TASK: Audit halaman blog.
CEK controller: filter kategori? Pagination? Related posts? SEO data? View counter?
CEK view: meta tags? OG tags? Tanggal format locale? Share buttons? Rich-text content? Related posts section? Pagination links? Featured post? Category filter active state?
Tulis fix."""
    },
    {
      "id": "FE-05", "title": "Bilingual (ID/EN) — SetLocale Middleware & Switcher",
      "target_files": ["app/Http/Middleware/SetLocale.php","resources/views/layouts/partials/lang-switcher.blade.php","app/Http/Controllers/Frontend/LanguageSwitchController.php","app/Helpers/helpers.php","routes/web.php"],
      "prompt": """TASK: Audit implementasi bilingual.
CEK middleware: baca dari session & URL segment? App::setLocale()?
CEK helpers: locale_field(), setting(), active_locale(), is_locale_en()?
CEK switcher: link generate benar? Current locale active visual?
CEK view: semua teks pakai locale_field()? Static string pakai lang file?
Tulis fix untuk inkonsistensi."""
    },
    {
      "id": "FE-06", "title": "SEO — Meta Tags, OG, Sitemap, Structured Data",
      "target_files": ["resources/views/layouts/partials/seo-head.blade.php","app/Services/SeoService.php","app/Console/Commands/GenerateSitemap.php","resources/views/layouts/app.blade.php"],
      "prompt": """TASK: Audit SEO implementation.
CEK seo-head: title, description, og:title/description/image/url, canonical, hreflang, robots?
CEK SeoService: Product schema JSON-LD? Article schema? Organization schema?
CEK GenerateSitemap: semua URL? loc/lastmod/changefreq/priority? public/sitemap.xml?
Tulis fix + JSON-LD Product schema."""
    },
  ],

  "code_standards": [
    {
      "id": "STD-01", "title": "Model Scopes & Relationships — Kelengkapan",
      "target_files": ["app/Models/Product.php","app/Models/BlogPost.php","app/Models/Career.php","app/Models/ContactMessage.php","app/Models/SiteSetting.php"],
      "prompt": """TASK: Audit model scopes, relationships, accessors.
CEK setiap model: scopeActive(), scopeOrdered(), scopeFeatured()/scopePublished()? Semua relasi? Accessor untuk URL/computed?
Tulis fix untuk model yang kurang."""
    },
    {
      "id": "STD-02", "title": "Controller — Response Consistency & Error Handling",
      "target_files": ["app/Http/Controllers/Admin/ProductController.php","app/Http/Controllers/Admin/BlogPostController.php","app/Http/Controllers/Admin/SiteSettingController.php"],
      "prompt": """TASK: Audit konsistensi response controller admin.
STANDAR: store->redirect index with success. update->redirect edit with success. destroy->redirect index with success. Try-catch setiap operasi. Slug auto dari name_id. Hapus file saat destroy.
Tulis fix."""
    },
    {
      "id": "STD-03", "title": "Blade Layout — Consistency & Component Reuse",
      "target_files": ["resources/views/layouts/app.blade.php","resources/views/layouts/admin.blade.php","resources/views/layouts/partials/navbar.blade.php","resources/views/layouts/partials/footer.blade.php","resources/views/layouts/partials/whatsapp-float.blade.php"],
      "prompt": """TASK: Audit Blade layout consistency.
CEK app: partial urutan? GA4 inject? @stack scripts/styles? Vite assets?
CEK navbar: menu dari DB? Active state? Hamburger Alpine? Lang switcher? CTA WA?
CEK footer: copyright date()? Social dari setting()? Sitemap link?
CEK WA float: scroll 100px? Nomor dari setting()? Default message? Pulse animasi?
Tulis fix."""
    },
  ],

  "performance": [
    {
      "id": "PERF-01", "title": "N+1 Query & Eager Loading Audit",
      "target_files": ["app/Http/Controllers/Frontend/HomeController.php","app/Http/Controllers/Frontend/ProductController.php","app/Http/Controllers/Frontend/BlogController.php"],
      "prompt": """TASK: Audit N+1 query problem.
CEK setiap method: ->get() atau ->paginate() tanpa with() yang diperlukan?
Home: featuredProducts with category,brand,primaryImage? Product index: with category? Product show: with category,brand,images,specs,highlights? Blog: with category,author?
Tulis fix + rekomendasi cache untuk site_settings."""
    },
  ],
}


# ═══════════════════════════════════════════════════════════════════════════
#  FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════

def load_file_contents(target_pattern: str) -> str:
    """Baca file berdasarkan glob pattern, return content untuk dikirim ke AI."""
    parts = []
    # Resolve glob
    if "**" in target_pattern or "*" in target_pattern:
        files = sorted(PROJECT_ROOT.glob(target_pattern))
    else:
        files = [PROJECT_ROOT / target_pattern]

    for f in files:
        if not f.is_file():
            continue
        try:
            content = f.read_text(encoding="utf-8")
            if len(content) > MAX_FILE_CHARS:
                content = content[:MAX_FILE_CHARS] + "\n# ... [truncated]"
            rel = f.relative_to(PROJECT_ROOT)
            parts.append(f"# FILE: {rel}\n```php\n{content}\n```")
        except Exception as e:
            parts.append(f"# FILE: {target_pattern}\n# ERROR: {e}")

    return "\n\n".join(parts) if parts else f"(file not found: {target_pattern})"


def call_api(prompt_text: str) -> str:
    """Panggil DeepSeek API dengan prompt."""
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {API_KEY}",
    }
    payload = json.dumps({
        "model": MODEL,
        "messages": [
            {"role": "system", "content": CONTEXT_HEADER},
            {"role": "user", "content": prompt_text},
        ],
        "max_tokens": 4096,
        "temperature": 0.3,
    }).encode("utf-8")

    req = urllib.request.Request(API_URL, data=payload, headers=headers, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=180) as resp:
            result = json.loads(resp.read().decode("utf-8"))
            return result["choices"][0]["message"]["content"]
    except urllib.error.HTTPError as e:
        body = e.read().decode("utf-8")
        return f"ERROR {e.code}: {body}"
    except Exception as e:
        return f"ERROR: {e}"


def run_prompt(prompt_data: dict, category: str, dry_run: bool = False) -> dict:
    prompt_id = prompt_data["id"]
    prompt_title = prompt_data["title"]
    target_files = prompt_data["target_files"]

    if dry_run:
        print(f"  [DRY RUN] {prompt_id} — {prompt_title}")
        return {"id": prompt_id, "status": "dry_run"}

    print(f"\n{'='*60}")
    print(f"[{prompt_id}] {prompt_title}")
    print(f"Files: {', '.join(target_files[:3])}")

    # Load target file contents
    file_contexts = []
    for pattern in target_files:
        fc = load_file_contents(pattern)
        file_contexts.append(fc)

    full_prompt = f"{prompt_data['prompt']}\n\n---\n## FILE CONTENTS:\n\n" + "\n\n".join(file_contexts)

    # Call API with retries
    for attempt in range(1, MAX_RETRIES + 1):
        print(f"  Calling API (attempt {attempt}/{MAX_RETRIES})...")
        start = time.time()
        output = call_api(full_prompt)
        elapsed = round(time.time() - start, 1)

        if output.startswith("ERROR"):
            print(f"  ✗ {output[:80]}...")
            if attempt < MAX_RETRIES:
                print(f"  Retrying in {RETRY_DELAY}s...")
                time.sleep(RETRY_DELAY)
            continue

        print(f"  ✓ Done in {elapsed}s — {len(output)} chars")
        break
    else:
        output = f"ERROR: Failed after {MAX_RETRIES} attempts"

    return {
        "id": prompt_id,
        "title": prompt_title,
        "category": category,
        "status": "ok" if not output.startswith("ERROR") else "error",
        "elapsed": elapsed,
        "output": output,
        "target_files": target_files,
        "timestamp": datetime.now().isoformat(),
    }


def save_result(result: dict):
    cat_dir = OUTPUT_DIR / result["category"]
    cat_dir.mkdir(parents=True, exist_ok=True)
    fname = cat_dir / f"{result['id']}.md"
    with open(fname, "w", encoding="utf-8") as f:
        f.write(f"# {result['id']} — {result['title']}\n\n")
        f.write(f"**Category:** {result['category']}  \n")
        f.write(f"**Status:** {result['status']}  \n")
        f.write(f"**Elapsed:** {result.get('elapsed', '?')}s  \n")
        f.write(f"**Timestamp:** {result['timestamp']}  \n\n")
        f.write(f"**Target Files:**\n")
        for tf in result["target_files"]:
            f.write(f"- `{tf}`\n")
        f.write(f"\n---\n\n## Output\n\n{result['output']}\n")
    return fname


def save_summary(results: list):
    summary_path = OUTPUT_DIR / "SUMMARY.md"
    total = len(results)
    ok = sum(1 for r in results if r["status"] == "ok")
    errors = sum(1 for r in results if r["status"] == "error")
    elapsed_total = sum(r.get("elapsed", 0) for r in results if r.get("elapsed"))

    with open(summary_path, "w", encoding="utf-8") as f:
        f.write("# CRIUS COMPRESSOR — Audit Summary\n\n")
        f.write(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}  \n")
        f.write(f"**Total:** {total} | ✓ OK: {ok} | ✗ Error: {errors} | ⏱ Total: {round(elapsed_total)}s\n\n")
        f.write("| # | ID | Category | Title | Status | Time |\n")
        f.write("|---|---|---|---|---|---|\n")
        for i, r in enumerate(results, 1):
            icon = "✓" if r["status"] == "ok" else ("⏱" if r["status"] == "timeout" else "✗")
            t = r.get("elapsed", "?")
            f.write(f"| {i} | {r['id']} | {r['category']} | {r['title']} | {icon} | {t}s |\n")

    print(f"\n{'='*60}")
    print(f"SUMMARY: {summary_path}")
    print(f"  {total} prompts | ✓ {ok} | ✗ {errors} | ⏱ {round(elapsed_total)}s")
    return summary_path


def main():
    parser = argparse.ArgumentParser(description="CRIUS Audit via DeepSeek API")
    parser.add_argument("category", nargs="?", default="all",
                        choices=["all"] + list(PROMPTS.keys()))
    parser.add_argument("--dry-run", action="store_true")
    parser.add_argument("--start-from", type=int, default=0)
    parser.add_argument("--delay", type=int, default=RATE_LIMIT_WAIT)
    args = parser.parse_args()

    if args.dry_run:
        # Skip API key check for dry run
        pass
    elif not API_KEY:
        print("ERROR: DEEPSEEK_API_KEY belum di-set!")
        print("  export DEEPSEEK_API_KEY=\"sk-your-key-here\"")
        print("  atau set DEEPSEEK_API_KEY=sk-your-key-here (Windows CMD)")
        print('  atau $env:DEEPSEEK_API_KEY="sk-your-key-here" (PowerShell)')
        sys.exit(1)

    OUTPUT_DIR.mkdir(exist_ok=True)

    if args.category == "all":
        queue = [(cat, p) for cat, prompts in PROMPTS.items() for p in prompts]
    else:
        queue = [(args.category, p) for p in PROMPTS[args.category]]

    total = len(queue)
    print(f"\n🔍 CRIUS AUDIT via DeepSeek API")
    print(f"   Category: {args.category} | Prompts: {total}")
    print(f"   Model: {MODEL}")
    print(f"   Output: {OUTPUT_DIR.absolute()}")
    if args.dry_run:
        print(f"   MODE: DRY RUN\n")

    results = []
    for i, (cat, prompt_data) in enumerate(queue):
        if i < args.start_from:
            continue

        print(f"\n[{i+1}/{total}] ", end="")
        result = run_prompt(prompt_data, cat, dry_run=args.dry_run)
        results.append(result)

        if not args.dry_run:
            saved = save_result(result)
            print(f"  Saved: {saved}")
            if i < total - 1 and result.get("status") == "ok":
                print(f"  Waiting {args.delay}s...")
                time.sleep(args.delay)

    if not args.dry_run:
        save_summary(results)
        json_path = OUTPUT_DIR / "results.json"
        with open(json_path, "w", encoding="utf-8") as f:
            json.dump(results, f, ensure_ascii=False, indent=2)

    print(f"\n{'='*60}")
    print(f"Selesai! Lihat hasil di: {OUTPUT_DIR.absolute()}\n")


if __name__ == "__main__":
    main()
