"""
Agent 4: VALIDATOR — Reviews and fixes critical files
Agent 5: TESTER   — Generates Jest test suites
Agent 6: PACKAGER — Writes to disk, creates zip, generates CI/CD
"""
from __future__ import annotations

import json
import zipfile
import asyncio
from pathlib import Path
from typing import Optional

from forge.core.session import GenerationSession, AppPlan
from forge.core.api import GroqClient, strip_fences

# ─────────────────────────────────────────────────────────────────────────────
# AGENT 4: VALIDATOR
# ─────────────────────────────────────────────────────────────────────────────

VALIDATOR_SYSTEM = """You are a Principal React Native engineer doing code review.
Fix ALL issues in the provided file. Be ruthless but precise.

Fix:
- Missing/wrong imports (use correct @react-navigation paths)
- TypeScript type errors (no implicit any)
- Broken navigation calls
- AsyncStorage API misuse
- Missing closing brackets/tags
- Undefined variables or components
- StyleSheet errors
- Placeholder content that slipped through

DO NOT: add new features, change architecture, add commentary.
Output ONLY the corrected complete file. No markdown fences."""


CRITICAL_FILE_TYPES = {"entry", "navigation", "context", "service", "type"}


async def run_validator(
    session: GenerationSession,
    groq: GroqClient,
) -> None:
    if session.settings.skip_validation:
        session.log("VALIDATOR", "Skipped (--skip-validation)")
        return

    plan = session.plan
    arch = session.architecture
    model = session.settings.validation_model

    agent = session.start_agent("VALIDATOR", f"Groq → {model}")
    session.log("VALIDATOR", "Running validation sweep on critical files…")

    # Find critical files
    files_spec = arch.get("files", [])
    critical = [
        f for f in files_spec
        if f.get("type") in CRITICAL_FILE_TYPES or f["path"] == arch.get("entryFile", "App.tsx")
    ]

    fixed_count = 0
    for file_info in critical[:6]:  # Cap to avoid rate limits
        path = file_info["path"]
        if path not in session.files:
            continue
        code = session.files[path].content
        if not code or "// Error generating" in code:
            continue

        session.log("VALIDATOR", f"Reviewing: {path}")
        try:
            nav_info = f"Navigation: {plan.navigation}, tab: {plan.tab_screens}, stack: {plan.stack_screens}"
            prompt = (
                f"App: {plan.app_name} | {nav_info}\n"
                f"All screens: {[s['name'] for s in plan.screens]}\n"
                f"Data models: {[m['name'] for m in plan.data_models]}\n"
                f"Storage key: {plan.storage_key}\n\n"
                f"File: {path}\n\n{code}"
            )
            fixed = await groq.complete(
                messages=[
                    {"role": "system", "content": VALIDATOR_SYSTEM},
                    {"role": "user", "content": prompt},
                ],
                model=model,
                temperature=0.05,
                max_tokens=6000,
            )
            fixed_code = strip_fences(fixed)
            session.files[path].content = fixed_code
            session.files[path].lines = fixed_code.count("\n") + 1
            fixed_count += 1
            session.log("VALIDATOR", f"  ✓ Fixed", "success")
        except Exception as e:
            session.log("VALIDATOR", f"  ⚠ Skipped: {e}", "warning")

    # Sanity check: detect files with obvious placeholders
    placeholder_files = []
    for path, fentry in session.files.items():
        if any(p in fentry.content for p in ["TODO:", "FIXME:", "lorem ipsum", "placeholder", "YOUR_"]):
            placeholder_files.append(path)

    if placeholder_files:
        session.log("VALIDATOR", f"⚠ Placeholder content in: {', '.join(placeholder_files)}", "warning")
    else:
        session.log("VALIDATOR", "✓ No placeholder content detected", "success")

    session.log("VALIDATOR", f"✓ {fixed_count} critical files validated & refined", "success")
    session.finish_agent(agent, fixed=fixed_count, checked=len(critical))


# ─────────────────────────────────────────────────────────────────────────────
# AGENT 5: TESTER
# ─────────────────────────────────────────────────────────────────────────────

TESTER_SYSTEM = """You are a Senior QA Engineer writing Jest + React Native Testing Library tests.
Generate comprehensive test suites that would pass code review at Google or Meta.

Rules:
- describe/it blocks with clear names
- Test happy path, edge cases, empty states, error states
- fireEvent for user interactions
- waitFor for async operations
- Mock: AsyncStorage, Navigation, date functions
- No placeholder assertions ('expect(true).toBe(true)')
- Output raw TypeScript. No markdown fences."""


async def run_tester(
    session: GenerationSession,
    groq: GroqClient,
) -> None:
    if not session.settings.include_tests:
        session.log("TESTER", "Skipped (include_tests=false)")
        return

    plan = session.plan
    arch = session.architecture
    model = session.settings.plan_model

    agent = session.start_agent("TESTER", f"Groq → {model}")
    session.log("TESTER", "Generating test suite…")

    # Pick top screens + context for testing
    files_spec = arch.get("files", [])
    testable = [
        f for f in files_spec
        if f.get("type") in ("screen", "context", "hook", "service") and f["path"] in session.files
    ][:5]  # Cap to 5 test files

    # Jest setup file
    setup = gen_jest_setup()
    session.add_ci("jest.setup.ts", setup)

    for file_info in testable:
        path = file_info["path"]
        source_code = session.files[path].content[:3000]
        test_path = f"__tests__/{Path(path).stem}.test.tsx"

        session.log("TESTER", f"Writing tests for: {path}")
        try:
            prompt = (
                f"App: {plan.app_name}\n"
                f"Data models: {json.dumps(plan.data_models[:2], indent=2)}\n"
                f"Source ({path}):\n{source_code}\n\n"
                f"Generate full test suite for {Path(path).stem}."
            )
            tests = await groq.complete(
                messages=[
                    {"role": "system", "content": TESTER_SYSTEM},
                    {"role": "user", "content": prompt},
                ],
                model=model,
                temperature=0.1,
                max_tokens=3000,
            )
            session.add_test(test_path, strip_fences(tests))
            session.log("TESTER", f"  ✓ {test_path}", "success")
        except Exception as e:
            session.log("TESTER", f"  ⚠ {path}: {e}", "warning")

    session.finish_agent(agent, tests=len(session.test_files))


def gen_jest_setup() -> str:
    return """import '@testing-library/react-native/extend-expect';

// Mock AsyncStorage
jest.mock('@react-native-async-storage/async-storage', () =>
  require('@react-native-async-storage/async-storage/jest/async-storage-mock')
);

// Mock Expo modules
jest.mock('expo-haptics', () => ({ impactAsync: jest.fn(), notificationAsync: jest.fn() }));
jest.mock('expo-notifications', () => ({
  scheduleNotificationAsync: jest.fn(),
  cancelAllScheduledNotificationsAsync: jest.fn(),
  getPermissionsAsync: jest.fn(() => Promise.resolve({ status: 'granted' })),
}));

// Mock navigation
jest.mock('@react-navigation/native', () => {
  const actual = jest.requireActual('@react-navigation/native');
  return {
    ...actual,
    useNavigation: () => ({ navigate: jest.fn(), goBack: jest.fn(), push: jest.fn() }),
    useRoute: () => ({ params: {} }),
    useFocusEffect: jest.fn((cb) => cb()),
  };
});

// Silence console errors in tests
beforeAll(() => { jest.spyOn(console, 'error').mockImplementation(() => {}); });
afterAll(() => { (console.error as jest.Mock).mockRestore?.(); });
"""


# ─────────────────────────────────────────────────────────────────────────────
# AGENT 6: PACKAGER
# ─────────────────────────────────────────────────────────────────────────────

def gen_github_actions_ci(plan: AppPlan) -> str:
    return f"""name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    name: Test & Type Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run type-check
      - run: npm run test:ci
        env:
          CI: true

  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npx expo-env-info 2>/dev/null || true

  build-preview:
    name: EAS Build Preview
    runs-on: ubuntu-latest
    needs: [test, lint]
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{{{ secrets.EXPO_TOKEN }}}}
      - run: npm ci
      - run: eas build --platform all --profile preview --non-interactive
"""


def gen_eas_json(plan: AppPlan) -> str:
    return json.dumps({
        "cli": {"version": ">= 5.9.3"},
        "build": {
            "development": {
                "developmentClient": True,
                "distribution": "internal",
            },
            "preview": {
                "distribution": "internal",
                "channel": "preview",
            },
            "production": {
                "channel": "production",
                "ios": {"resourceClass": "m-medium"},
                "android": {"buildType": "apk"},
            },
        },
        "submit": {
            "production": {
                "ios": {"appleId": "your@email.com", "ascAppId": "your-app-id"},
                "android": {"serviceAccountKeyPath": "./google-services.json"},
            }
        },
    }, indent=2)


async def run_packager(
    session: GenerationSession,
) -> tuple[Path, Path]:
    plan = session.plan
    output_dir = Path(session.settings.output_dir).resolve()

    agent = session.start_agent("PACKAGER", "local disk I/O")
    session.log("PACKAGER", f"Writing project to disk: {output_dir / plan.slug}/")

    project_dir = output_dir / plan.slug
    all_files = session.all_files()

    # Add CI/CD files
    if session.settings.include_ci:
        session.add_ci(".github/workflows/ci.yml", gen_github_actions_ci(plan))
        session.add_ci("eas.json", gen_eas_json(plan))
        all_files = session.all_files()

    # Write to disk
    written = 0
    for rel_path, content in all_files.items():
        full_path = project_dir / rel_path
        full_path.parent.mkdir(parents=True, exist_ok=True)
        full_path.write_text(content, encoding="utf-8")
        written += 1

    session.log("PACKAGER", f"  ✓ {written} files written to disk")

    # Create zip
    zip_path = output_dir / f"{plan.slug}.zip"
    session.log("PACKAGER", f"  Creating {zip_path.name}…")

    with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
        for rel_path in all_files:
            full_path = project_dir / rel_path
            if full_path.exists():
                zf.write(full_path, arcname=f"{plan.slug}/{rel_path}")

    zip_size_mb = zip_path.stat().st_size / (1024 * 1024)
    session.log("PACKAGER", f"  ✓ {zip_path.name} ({zip_size_mb:.2f} MB)", "success")

    session.project_dir = project_dir
    session.zip_path = zip_path

    session.finish_agent(
        agent,
        files=written,
        zip_mb=round(zip_size_mb, 2),
    )

    return project_dir, zip_path
