#!/usr/bin/env python3
"""
CRIUS COMPRESSOR — Auto-Fix Runner
Membaca hasil audit dari audit_results/ dan menerapkan perbaikan otomatis.
Jalan sendiri, tanpa perlu external AI / CLI.

Usage:
  python run_fix.py                       # Terapkan semua fix (konfirmasi tiap file)
  python run_fix.py --auto                # Terapkan semua tanpa konfirmasi
  python run_fix.py --dry-run             # Lihat perubahan tanpa eksekusi
  python run_fix.py --list                # Daftar fix yang tersedia
"""

import sys
import os
import re
import argparse
from pathlib import Path
from datetime import datetime

PROJECT_ROOT = Path(".")

# ─── REGISTERED FIXES ───────────────────────────────────────────────────
# Setiap fix punya: id, file target, deskripsi, dan fungsi apply()
# ────────────────────────────────────────────────────────────────────────

FIXES = []

def register(id, title, filepath, description, apply_func):
    FIXES.append({
        "id": id,
        "title": title,
        "filepath": filepath,
        "description": description,
        "apply": apply_func,
    })


# ═══════════════════════════════════════════════════════════════════════════
#  SEC-02: Uncomment role check di AdminAuth middleware
# ═══════════════════════════════════════════════════════════════════════════

def fix_sec02(fp):
    content = fp.read_text(encoding="utf-8")
    old = """        // Opsional: Cek role jika ada dashboard khusus admin/staff
        // if (Auth::user()->role === 'user') {
        //     abort(403, 'Unauthorized Access.');
        // }"""
    new = """        if (!Auth::user()->is_active) {
            Auth::logout();
            return redirect()->route('admin.login');
        }"""
    if old in content:
        content = content.replace(old, new)
        fp.write_text(content, encoding="utf-8")
        return True
    return False

register("SEC-02", "Aktifkan role check + is_active di AdminAuth",
    "app/Http/Middleware/AdminAuth.php",
    "Menambahkan pengecekan is_active setelah Auth::check()",
    fix_sec02)


# ═══════════════════════════════════════════════════════════════════════════
#  SEC-03: Tambah MIME validation + path validation di MediaUploadService
# ═══════════════════════════════════════════════════════════════════════════

def fix_sec03_mime(fp):
    content = fp.read_text(encoding="utf-8")
    old = """    protected function validateFile(UploadedFile $file): void
    {
        $extension = strtolower($file->getClientOriginalExtension());

        if (!in_array($extension, $this->allowedTypes)) {
            throw new \\InvalidArgumentException("File type '{$extension}' is not allowed.");
        }

        if ($file->getSize() > $this->maxSizeKb * 1024) {
            throw new \\InvalidArgumentException("File exceeds maximum size of {$this->maxSizeKb} KB.");
        }
    }"""
    new = """    protected function validateFile(UploadedFile $file): void
    {
        $extension = strtolower($file->getClientOriginalExtension());

        if (!in_array($extension, $this->allowedTypes)) {
            throw new \\InvalidArgumentException("File type '{$extension}' is not allowed.");
        }

        $allowedMimes = [
            'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg',
            'png' => 'image/png', 'webp' => 'image/webp',
            'gif' => 'image/gif', 'svg' => 'image/svg+xml',
            'pdf' => 'application/pdf',
        ];

        if (isset($allowedMimes[$extension]) && $allowedMimes[$extension] !== $file->getMimeType()) {
            throw new \\InvalidArgumentException("File MIME type does not match extension.");
        }

        if ($file->getSize() > $this->maxSizeKb * 1024) {
            throw new \\InvalidArgumentException("File exceeds maximum size of {$this->maxSizeKb} KB.");
        }
    }"""
    if old in content:
        content = content.replace(old, new)
        fp.write_text(content, encoding="utf-8")
        return True
    return False

register("SEC-03a", "Tambah MIME validation di MediaUploadService",
    "app/Services/MediaUploadService.php",
    "Validasi MIME type (bukan cuma ekstensi)",
    fix_sec03_mime)


def fix_sec03_delete(fp):
    content = fp.read_text(encoding="utf-8")
    old = """    public function delete(string $path): bool
    {
        $fullPath = public_path(ltrim($path, '/'));
        if (file_exists($fullPath)) {
            return unlink($fullPath);
        }
        return false;
    }"""
    new = """    public function delete(string $path): bool
    {
        $clean = ltrim($path, '/');
        $allowedPrefixes = ['images/products/', 'images/blog/', 'images/brands/', 'images/services/', 'images/team/', 'images/general/'];
        $isAllowed = false;
        foreach ($allowedPrefixes as $prefix) {
            if (str_starts_with($clean, $prefix)) {
                $isAllowed = true;
                break;
            }
        }
        if (!$isAllowed) {
            throw new \\InvalidArgumentException("Cannot delete files outside upload directories.");
        }
        $fullPath = public_path($clean);
        if (file_exists($fullPath)) {
            return unlink($fullPath);
        }
        return false;
    }"""
    if old in content:
        content = content.replace(old, new)
        fp.write_text(content, encoding="utf-8")
        return True
    return False

register("SEC-03b", "Tambah path validation di MediaUploadService::delete()",
    "app/Services/MediaUploadService.php",
    "Mencegah path traversal saat hapus file",
    fix_sec03_delete)


# ═══════════════════════════════════════════════════════════════════════════
#  SEC-06: Tambah rate limiting di routes
# ═══════════════════════════════════════════════════════════════════════════

def fix_sec06_throttle(fp):
    content = fp.read_text(encoding="utf-8")
    # Cari route contact.send dan careers.apply
    old_contact = "Route::post('/contact', [ContactController::class, 'send'])->name('contact.send');"
    new_contact = "Route::post('/contact', [ContactController::class, 'send'])->middleware('throttle:5,1')->name('contact.send');"
    old_career = "Route::post('/careers/{id}/apply', [CareerController::class, 'apply'])->name('careers.apply');"
    new_career = "Route::post('/careers/{id}/apply', [CareerController::class, 'apply'])->middleware('throttle:3,1')->name('careers.apply');"

    changed = False
    if old_contact in content:
        content = content.replace(old_contact, new_contact)
        changed = True
    if old_career in content:
        content = content.replace(old_career, new_career)
        changed = True
    if changed:
        fp.write_text(content, encoding="utf-8")
    return changed

register("SEC-06a", "Tambah throttle middleware di routes POST contact & career",
    "routes/web.php",
    "Mencegah spam: 5 req/menit ke contact, 3 req/menit ke career",
    fix_sec06_throttle)


def fix_sec06_message_max(fp):
    content = fp.read_text(encoding="utf-8")
    old = "'message' => 'required|string|min:10',"
    new = "'message' => 'required|string|min:10|max:5000',"
    if old in content:
        content = content.replace(old, new)
        fp.write_text(content, encoding="utf-8")
        return True
    return False

register("SEC-06b", "Tambah max:5000 di ContactFormRequest message",
    "app/Http/Requests/Frontend/ContactFormRequest.php",
    "Mencegah input pesan terlalu panjang",
    fix_sec06_message_max)


def fix_sec06_career_formrequest(fp):
    content = fp.read_text(encoding="utf-8")
    old = """    public function apply(Request $request, $id)
    {
        $career = Career::findOrFail((int) $id);
        abort_unless($career->is_active && (!$career->expired_at || $career->expired_at > now()), 404);

        $data = $request->validate([
            'full_name' => 'required|string|max:255',
            'email' => 'required|email|max:255',
            'phone' => 'nullable|string|max:50',
            'cover_letter' => 'nullable|string',
            'resume_path' => 'nullable|string|max:255',
        ]);"""
    new = """    public function apply(CareerApplyRequest $request, $id)
    {
        $career = Career::findOrFail((int) $id);
        abort_unless($career->is_active && (!$career->expired_at || $career->expired_at > now()), 404);

        $data = $request->validated();"""
    # Check if CareerApplyRequest is already imported
    import_match = "use App\\Http\\Requests\\Frontend\\CareerApplyRequest;"
    if import_match not in content:
        # Add import after the existing use statements
        content = content.replace(
            "use App\\Http\\Controllers\\Controller;",
            "use App\\Http\\Controllers\\Controller;\n" + import_match
        )
    if old in content:
        content = content.replace(old, new)
        fp.write_text(content, encoding="utf-8")
        return True
    return False

register("SEC-06c", "CareerController pakai CareerApplyRequest",
    "app/Http/Controllers/Frontend/CareerController.php",
    "Ganti inline $request->validate() dengan Form Request",
    fix_sec06_career_formrequest)


# ═══════════════════════════════════════════════════════════════════════════
#  ADM-02: Tambah @error() directives di form admin
# ═══════════════════════════════════════════════════════════════════════════

def fix_adm02_blog_form(fp):
    content = fp.read_text(encoding="utf-8")
    # Tambah @error setelah setiap input
    # Slug field
    old = """                    <input type="text" name="slug" class="form-input" style="background:var(--white);color:var(--navy);border-color:var(--grey-200);" value="{{ old('slug', $post->slug ?? '') }}" required>
                </div>"""
    new = """                    <input type="text" name="slug" class="form-input" style="background:var(--white);color:var(--navy);border-color:var(--grey-200);" value="{{ old('slug', $post->slug ?? '') }}" required>
                    @error('slug')<p class="text-red-500 text-xs mt-1">{{ $message }}</p>@enderror
                </div>"""
    if old in content:
        content = content.replace(old, new)
        fp.write_text(content, encoding="utf-8")
        return True
    return False

register("ADM-02", "Tambah @error() di blog posts form",
    "resources/views/admin/blog/posts/form.blade.php",
    "Menampilkan error validasi di bawah setiap input (slug, title, dll)",
    fix_adm02_blog_form)


# ═══════════════════════════════════════════════════════════════════════════
#  ADM-01: Tambah admin-table-actions class di blog posts table
# ═══════════════════════════════════════════════════════════════════════════

def fix_adm01_blog_table(fp):
    content = fp.read_text(encoding="utf-8")
    old = """                    <td><a href="{{ route('admin.blog-posts.edit', $post) }}" class="action-btn action-btn--edit">Edit</a></td>"""
    new = """                    <td class="admin-table-actions">
                        <a href="{{ route('admin.blog-posts.edit', $post) }}" class="action-btn action-btn--edit">Edit</a>
                        <form method="POST" action="{{ route('admin.blog-posts.destroy', $post) }}" onsubmit="return confirm('Hapus?')" style="display:inline">
                            @csrf @method('DELETE')
                            <button type="submit" class="action-btn action-btn--danger">Hapus</button>
                        </form>
                    </td>"""
    if old in content:
        content = content.replace(old, new)
        fp.write_text(content, encoding="utf-8")
        return True
    return False

register("ADM-01", "Tambah admin-table-actions + tombol hapus di blog posts table",
    "resources/views/admin/blog/posts/index.blade.php",
    "Konsistensi dengan tabel admin lainnya + fitur hapus langsung",
    fix_adm01_blog_table)


# ═══════════════════════════════════════════════════════════════════════════
#  FE-01: Fix orderBy di HomeController
# ═══════════════════════════════════════════════════════════════════════════

def fix_fe01_order(fp):
    content = fp.read_text(encoding="utf-8")
    old = """        $latestPosts = BlogPost::with('category')
            ->where('status', 'published')
            ->orderBy('created_at', 'desc')
            ->take(3)
            ->get();"""
    new = """        $latestPosts = BlogPost::with('category')
            ->where('status', 'published')
            ->where('published_at', '<=', now())
            ->orderBy('published_at', 'desc')
            ->take(3)
            ->get();"""
    if old in content:
        content = content.replace(old, new)
        fp.write_text(content, encoding="utf-8")
        return True
    return False

register("FE-01", "Fix orderBy di HomeController — pakai published_at",
    "app/Http/Controllers/Frontend/HomeController.php",
    "Scheduled posting: hanya tampilkan artikel yang publish date-nya sudah lewat",
    fix_fe01_order)


# ═══════════════════════════════════════════════════════════════════════════
#  FE-04: Fix orderBy di BlogController
# ═══════════════════════════════════════════════════════════════════════════

def fix_fe04_order(fp):
    content = fp.read_text(encoding="utf-8")
    old = """        $posts = BlogPost::with(['category', 'author'])
            ->where('status', 'published')
            ->orderBy('created_at', 'desc')
            ->paginate(9);"""
    new = """        $posts = BlogPost::with(['category', 'author'])
            ->where('status', 'published')
            ->where('published_at', '<=', now())
            ->orderBy('published_at', 'desc')
            ->paginate(9);"""
    if old in content:
        content = content.replace(old, new)
        fp.write_text(content, encoding="utf-8")
        return True
    return False

register("FE-04", "Fix orderBy di BlogController index — pakai published_at",
    "app/Http/Controllers/Frontend/BlogController.php",
    "Scheduled posting untuk blog listing",
    fix_fe04_order)


# ═══════════════════════════════════════════════════════════════════════════
#  FE-06: Tambah Open Graph tags di app.blade.php
# ═══════════════════════════════════════════════════════════════════════════

def fix_fe06_og_tags(fp):
    content = fp.read_text(encoding="utf-8")
    # Cari posisi setelah meta description
    marker = '<meta name="description" content="@yield(\'meta_description\', setting(locale_field(\'meta_description\'), \'PT Crius Sinergi Indonesia - Solusi Kompresor Angin Industrial Terbaik\'))">'
    og_tags = """<meta name="description" content="@yield('meta_description', setting(locale_field('meta_description'), 'PT Crius Sinergi Indonesia - Solusi Kompresor Angin Industrial Terbaik'))">

    <!-- Open Graph -->
    <meta property="og:title" content="@yield('og_title', strip_tags(setting(locale_field('hero_title'), 'Crius Compressor')))">
    <meta property="og:description" content="@yield('og_description', strip_tags(setting(locale_field('meta_description'))))">
    <meta property="og:image" content="@yield('og_image', asset(setting('default_og_image', 'images/og-default.jpg')))">
    <meta property="og:url" content="{{ url()->current() }}">
    <meta property="og:type" content="@yield('og_type', 'website')">
    <link rel="canonical" href="{{ url()->current() }}">
    <meta name="robots" content="@yield('robots', 'index, follow')">"""

    if marker in content:
        content = content.replace(marker, og_tags)
        fp.write_text(content, encoding="utf-8")
        return True
    return False

register("FE-06", "Tambah Open Graph + canonical + robots tags di layout",
    "resources/views/layouts/app.blade.php",
    "SEO: og:title, og:description, og:image, canonical URL, robots meta",
    fix_fe06_og_tags)


# ═══════════════════════════════════════════════════════════════════════════
#  RUNNER
# ═══════════════════════════════════════════════════════════════════════════

def backup_file(filepath):
    """Buat backup .bak sebelum modifikasi."""
    bak = filepath.with_suffix(filepath.suffix + ".bak")
    if not bak.exists():
        import shutil
        shutil.copy2(filepath, bak)
        return bak
    return bak


def main():
    parser = argparse.ArgumentParser(description="CRIUS Auto-Fix Runner")
    parser.add_argument("--auto", action="store_true", help="Terapkan semua tanpa konfirmasi")
    parser.add_argument("--dry-run", action="store_true", help="Lihat perubahan tanpa eksekusi")
    parser.add_argument("--list", action="store_true", help="Daftar fix yang tersedia")
    parser.add_argument("--fix", type=str, help="Jalankan fix spesifik (ID)")
    args = parser.parse_args()

    if args.list:
        print(f"\n{'='*60}")
        print(f"Tersedia {len(FIXES)} fix:\n")
        for i, fix in enumerate(FIXES, 1):
            print(f"  [{i:2d}] {fix['id']:8s} | {fix['title']}")
            print(f"       File: {fix['filepath']}")
            print(f"       {fix['description']}")
            print()
        return

    # Filter fix spesifik jika diminta
    if args.fix:
        to_run = [f for f in FIXES if f["id"] == args.fix]
        if not to_run:
            print(f"Fix '{args.fix}' tidak ditemukan. Gunakan --list")
            sys.exit(1)
    else:
        to_run = FIXES

    print(f"\n{'='*60}")
    print(f"CRIUS AUTO-FIX RUNNER")
    print(f"Total fix: {len(to_run)} | Mode: {'DRY RUN' if args.dry_run else 'LIVE'}")
    if args.auto:
        print("Auto-confirm: ON")
    print()

    applied = 0
    skipped = 0
    errors = 0

    for fix in to_run:
        fp = PROJECT_ROOT / fix["filepath"]
        if not fp.exists():
            print(f"  [SKIP] {fix['id']} — File tidak ditemukan: {fix['filepath']}")
            skipped += 1
            continue

        # Preview
        print(f"\n{'─'*60}")
        print(f"  [{fix['id']}] {fix['title']}")
        print(f"  File: {fix['filepath']}")
        print(f"  {fix['description']}")

        if args.dry_run:
            modified = fix["apply"](fp) if fp.exists() else False
            status = "⚠ AKAN DIUBAH" if modified else "✓ SUDAH OK"
            print(f"  → {status}")
            if modified:
                # Restore from git
                import subprocess
                subprocess.run(["git", "checkout", "--", str(fp)], capture_output=True, cwd=PROJECT_ROOT)
            applied += 1
            continue

        # Konfirmasi
        if not args.auto:
            resp = input(f"  Terapkan? [Y/n/s(kip)] ").strip().lower()
            if resp == 'n' or resp == 's':
                print(f"  → SKIP")
                skipped += 1
                continue

        # Backup
        bak = backup_file(fp)

        # Apply
        try:
            modified = fix["apply"](fp)
            if modified:
                print(f"  ✓ TERAPKAN. Backup: {bak}")
                applied += 1
            else:
                print(f"  ✓ SUDAH OK (tidak ada perubahan)")
                # Restore backup jika tidak ada perubahan
                import shutil
                shutil.copy2(bak, fp)
                bak.unlink()
                applied += 1
        except Exception as e:
            print(f"  ✗ ERROR: {e}")
            # Restore dari backup
            import shutil
            shutil.copy2(bak, fp)
            bak.unlink()
            errors += 1

    print(f"\n{'='*60}")
    print(f"SELESAI: {applied} applied | {skipped} skipped | {errors} errors")
    print(f"Backup file (.bak) tersimpan di folder masing-masing.")


if __name__ == "__main__":
    main()
