#!/usr/bin/env python3
"""
matrix_glitch_detect.py — 3D World Visual Artifact Detection for The Matrix.

Scans screenshots or live pages for visual glitches: floating assets, z-fighting,
texture pop-in, clipping, broken meshes, lighting artifacts. Outputs structured
JSON, text, or standalone HTML report with annotated screenshots.

Usage:
    # Scan a screenshot
    python scripts/matrix_glitch_detect.py --image screenshot.png

    # Scan with vision model
    python scripts/matrix_glitch_detect.py --image screenshot.png --vision

    # HTML report
    python scripts/matrix_glitch_detect.py --image screenshot.png --html report.html

    # Scan live Matrix page
    python scripts/matrix_glitch_detect.py --url https://matrix.alexanderwhitestone.com

    # Batch scan a directory
    python scripts/matrix_glitch_detect.py --batch ./screenshots/ --html batch-report.html

Refs: timmy-config#491, #541, #543, #544
"""

from __future__ import annotations

import argparse
import base64
import html as html_module
import json
import os
import sys
import time
import urllib.error
import urllib.request
from dataclasses import dataclass, field, asdict
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Optional


# === Configuration ===

OLLAMA_BASE = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434")
VISION_MODEL = os.environ.get("VISUAL_REVIEW_MODEL", "gemma3:12b")


class Severity(str, Enum):
    CRITICAL = "critical"
    MAJOR = "major"
    MINOR = "minor"
    COSMETIC = "cosmetic"


@dataclass
class Glitch:
    """A single detected visual artifact."""
    type: str = ""           # floating_asset, z_fighting, texture_pop, clipping, lighting, mesh_break
    severity: Severity = Severity.MINOR
    region: str = ""         # "upper-left", "center", "bottom-right", or coordinates
    description: str = ""
    confidence: float = 0.0  # 0.0-1.0
    source: str = ""         # "programmatic", "vision", "pixel_analysis"


@dataclass
class GlitchReport:
    """Complete glitch detection report."""
    source: str = ""         # file path or URL
    timestamp: str = ""
    status: str = "PASS"     # PASS, WARN, FAIL
    score: int = 100
    glitches: list[Glitch] = field(default_factory=list)
    summary: str = ""
    model_used: str = ""
    width: int = 0
    height: int = 0


# === Programmatic Analysis ===

def analyze_pixels(image_path: str) -> list[Glitch]:
    """Programmatic pixel analysis for common 3D glitches."""
    glitches = []

    try:
        from PIL import Image
        img = Image.open(image_path).convert("RGB")
        w, h = img.size
        pixels = img.load()

        # Check for solid-color regions (render failure)
        corner_colors = [
            pixels[0, 0], pixels[w-1, 0], pixels[0, h-1], pixels[w-1, h-1]
        ]
        if all(c == corner_colors[0] for c in corner_colors):
            # All corners same color — check if it's black (render failure)
            if corner_colors[0] == (0, 0, 0):
                glitches.append(Glitch(
                    type="render_failure",
                    severity=Severity.CRITICAL,
                    region="entire frame",
                    description="Entire frame is black — 3D scene failed to render",
                    confidence=0.9,
                    source="pixel_analysis"
                ))

        # Check for horizontal tearing lines
        tear_count = 0
        for y in range(0, h, max(1, h // 20)):
            row_start = pixels[0, y]
            same_count = sum(1 for x in range(w) if pixels[x, y] == row_start)
            if same_count > w * 0.95:
                tear_count += 1
        if tear_count > 3:
            glitches.append(Glitch(
                type="horizontal_tear",
                severity=Severity.MAJOR,
                region=f"{tear_count} lines",
                description=f"Horizontal tearing detected — {tear_count} mostly-solid scanlines",
                confidence=0.7,
                source="pixel_analysis"
            ))

        # Check for extreme brightness variance (lighting artifacts)
        import statistics
        brightness_samples = []
        for y in range(0, h, max(1, h // 50)):
            for x in range(0, w, max(1, w // 50)):
                r, g, b = pixels[x, y]
                brightness_samples.append(0.299 * r + 0.587 * g + 0.114 * b)
        if brightness_samples:
            stdev = statistics.stdev(brightness_samples)
            if stdev > 100:
                glitches.append(Glitch(
                    type="lighting",
                    severity=Severity.MINOR,
                    region="global",
                    description=f"Extreme brightness variance (stdev={stdev:.0f}) — possible lighting artifacts",
                    confidence=0.5,
                    source="pixel_analysis"
                ))

    except ImportError:
        pass  # PIL not available
    except Exception as e:
        pass

    return glitches


# === Vision Analysis ===

GLITCH_VISION_PROMPT = """You are a 3D world QA engineer. Analyze this screenshot from a Three.js 3D world (The Matrix) for visual glitches and artifacts.

Look for these specific issues:

1. FLOATING ASSETS: Objects hovering above surfaces where they should rest. Look for shadows detached from objects.

2. Z-FIGHTING: Flickering or shimmering surfaces where two polygons overlap at the same depth. Usually appears as striped or dithered patterns.

3. TEXTURE POP-IN: Low-resolution textures that haven't loaded, or textures that suddenly change quality between frames.

4. CLIPPING: Objects passing through walls, floors, or other objects. Characters partially inside geometry.

5. LIGHTING ARTIFACTS: Hard light seams, black patches, overexposed areas, lights not illuminating correctly.

6. MESH BREAKS: Visible seams in geometry, missing faces on 3D objects, holes in surfaces.

7. RENDER FAILURE: Black areas where geometry should be, missing skybox, incomplete frame rendering.

8. UI OVERLAP: UI elements overlapping 3D viewport incorrectly.

Respond as JSON:
{
    "glitches": [
        {
            "type": "floating_asset|z_fighting|texture_pop|clipping|lighting|mesh_break|render_failure|ui_overlap",
            "severity": "critical|major|minor|cosmetic",
            "region": "description of where",
            "description": "detailed description of the artifact",
            "confidence": 0.0-1.0
        }
    ],
    "overall_quality": 0-100,
    "summary": "brief assessment"
}"""


def run_vision_analysis(image_path: str, model: str = VISION_MODEL) -> tuple[list[Glitch], int]:
    """Run vision model glitch analysis."""
    try:
        b64 = base64.b64encode(Path(image_path).read_bytes()).decode()
        payload = json.dumps({
            "model": model,
            "messages": [{"role": "user", "content": [
                {"type": "text", "text": GLITCH_VISION_PROMPT},
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
            ]}],
            "stream": False,
            "options": {"temperature": 0.1}
        }).encode()

        req = urllib.request.Request(
            f"{OLLAMA_BASE}/api/chat",
            data=payload,
            headers={"Content-Type": "application/json"}
        )
        with urllib.request.urlopen(req, timeout=120) as resp:
            result = json.loads(resp.read())
            content = result.get("message", {}).get("content", "")

        parsed = _parse_json_response(content)
        glitches = []
        for g in parsed.get("glitches", []):
            glitches.append(Glitch(
                type=g.get("type", "unknown"),
                severity=Severity(g.get("severity", "minor")),
                region=g.get("region", ""),
                description=g.get("description", ""),
                confidence=float(g.get("confidence", 0.5)),
                source="vision"
            ))
        return glitches, parsed.get("overall_quality", 80)

    except Exception as e:
        print(f"  Vision analysis failed: {e}", file=sys.stderr)
        return [], 50


def _parse_json_response(text: str) -> dict:
    cleaned = text.strip()
    if cleaned.startswith("```"):
        lines = cleaned.split("\n")[1:]
        if lines and lines[-1].strip() == "```":
            lines = lines[:-1]
        cleaned = "\n".join(lines)
    try:
        return json.loads(cleaned)
    except json.JSONDecodeError:
        start = cleaned.find("{")
        end = cleaned.rfind("}")
        if start >= 0 and end > start:
            try:
                return json.loads(cleaned[start:end + 1])
            except json.JSONDecodeError:
                pass
    return {}


# === Screenshot Capture ===

def capture_screenshot(url: str, output_path: str) -> bool:
    """Take a screenshot of a URL."""
    try:
        script = f"""
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page(viewport={{"width": 1280, "height": 720}})
    page.goto("{url}", wait_until="networkidle", timeout=30000)
    page.wait_for_timeout(3000)
    page.screenshot(path="{output_path}")
    browser.close()
"""
        result = subprocess.run(["python3", "-c", script], capture_output=True, text=True, timeout=60)
        return result.returncode == 0 and Path(output_path).exists()
    except Exception:
        return False


# === Detection Logic ===

def detect_glitches(image_path: str, use_vision: bool = False,
                    model: str = VISION_MODEL) -> GlitchReport:
    """Run full glitch detection on an image."""
    report = GlitchReport(
        source=image_path,
        timestamp=datetime.now().isoformat(),
        model_used=model if use_vision else "none"
    )

    if not Path(image_path).exists():
        report.status = "FAIL"
        report.summary = f"File not found: {image_path}"
        report.score = 0
        return report

    # Get image dimensions
    try:
        from PIL import Image
        img = Image.open(image_path)
        report.width, report.height = img.size
    except Exception:
        pass

    # Programmatic analysis
    prog_glitches = analyze_pixels(image_path)
    report.glitches.extend(prog_glitches)

    # Vision analysis
    if use_vision:
        print(f"  Running vision analysis on {image_path}...", file=sys.stderr)
        vision_glitches, quality = run_vision_analysis(image_path, model)
        report.glitches.extend(vision_glitches)
        report.score = quality
    else:
        # Score based on programmatic results
        criticals = sum(1 for g in report.glitches if g.severity == Severity.CRITICAL)
        majors = sum(1 for g in report.glitches if g.severity == Severity.MAJOR)
        report.score = max(0, 100 - criticals * 40 - majors * 15)

    # Determine status
    criticals = sum(1 for g in report.glitches if g.severity == Severity.CRITICAL)
    majors = sum(1 for g in report.glitches if g.severity == Severity.MAJOR)

    if criticals > 0:
        report.status = "FAIL"
    elif majors > 0 or report.score < 70:
        report.status = "WARN"
    else:
        report.status = "PASS"

    report.summary = (
        f"{report.status}: {len(report.glitches)} glitch(es) found "
        f"({criticals} critical, {majors} major), score {report.score}/100"
    )

    return report


# === HTML Report Generator ===

def generate_html_report(reports: list[GlitchReport], title: str = "Glitch Detection Report") -> str:
    """Generate a standalone HTML report with annotated details."""
    total_glitches = sum(len(r.glitches) for r in reports)
    total_criticals = sum(sum(1 for g in r.glitches if g.severity == Severity.CRITICAL) for r in reports)
    avg_score = sum(r.score for r in reports) // max(1, len(reports))

    if total_criticals > 0:
        overall_verdict = "FAIL"
        verdict_color = "#f44336"
    elif any(r.status == "WARN" for r in reports):
        overall_verdict = "WARN"
        verdict_color = "#ff9800"
    else:
        overall_verdict = "PASS"
        verdict_color = "#4caf50"

    # Build HTML
    parts = []
    parts.append(f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{html_module.escape(title)}</title>
<style>
*{{margin:0;padding:0;box-sizing:border-box}}
body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,monospace;background:#0a0a14;color:#c0c0d0;font-size:13px;line-height:1.5}}
.container{{max-width:1000px;margin:0 auto;padding:20px}}
header{{text-align:center;padding:24px 0;border-bottom:1px solid #1a1a2e;margin-bottom:24px}}
header h1{{font-size:20px;font-weight:300;letter-spacing:3px;color:#4a9eff;margin-bottom:8px}}
.verdict{{display:inline-block;padding:6px 20px;border-radius:4px;font-size:14px;font-weight:700;letter-spacing:2px;color:#fff;background:{verdict_color}}}
.stats{{display:flex;gap:16px;justify-content:center;margin:16px 0;flex-wrap:wrap}}
.stat{{background:#0e0e1a;border:1px solid #1a1a2e;border-radius:4px;padding:8px 16px;text-align:center}}
.stat .val{{font-size:20px;font-weight:700;color:#4a9eff}}
.stat .lbl{{font-size:9px;color:#666;text-transform:uppercase;letter-spacing:1px}}
.score-gauge{{width:120px;height:120px;margin:0 auto 16px;position:relative}}
.score-gauge svg{{transform:rotate(-90deg)}}
.score-gauge .score-text{{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:28px;font-weight:700}}
.report-card{{background:#0e0e1a;border:1px solid #1a1a2e;border-radius:6px;margin-bottom:16px;overflow:hidden}}
.report-header{{padding:12px 16px;border-bottom:1px solid #1a1a2e;display:flex;justify-content:space-between;align-items:center}}
.report-header .source{{color:#4a9eff;font-weight:600;word-break:break-all}}
.report-header .status-badge{{padding:2px 10px;border-radius:3px;font-size:11px;font-weight:700;color:#fff}}
.status-pass{{background:#4caf50}}
.status-warn{{background:#ff9800}}
.status-fail{{background:#f44336}}
.screenshot{{text-align:center;padding:12px;background:#080810}}
.screenshot img{{max-width:100%;max-height:400px;border:1px solid #1a1a2e;border-radius:4px}}
.glitch-list{{padding:12px 16px}}
.glitch-item{{padding:8px 0;border-bottom:1px solid #111;display:flex;gap:12px;align-items:flex-start}}
.glitch-item:last-child{{border-bottom:none}}
.severity-dot{{width:8px;height:8px;border-radius:50%;margin-top:5px;flex-shrink:0}}
.sev-critical{{background:#f44336}}
.sev-major{{background:#ff9800}}
.sev-minor{{background:#2196f3}}
.sev-cosmetic{{background:#666}}
.glitch-detail{{flex:1}}
.glitch-type{{color:#ffd700;font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:1px}}
.glitch-desc{{color:#aaa;font-size:12px;margin-top:2px}}
.glitch-meta{{color:#555;font-size:10px;margin-top:2px}}
.no-glitches{{color:#4caf50;text-align:center;padding:20px;font-style:italic}}
footer{{text-align:center;padding:16px;color:#444;font-size:10px;border-top:1px solid #1a1a2e;margin-top:24px}}
</style>
</head>
<body>
<div class="container">
<header>
<h1>{html_module.escape(title)}</h1>
<div class="verdict">{overall_verdict}</div>
<div class="stats">
  <div class="stat"><div class="val">{len(reports)}</div><div class="lbl">Screenshots</div></div>
  <div class="stat"><div class="val">{total_glitches}</div><div class="lbl">Glitches</div></div>
  <div class="stat"><div class="val">{total_criticals}</div><div class="lbl">Critical</div></div>
  <div class="stat"><div class="val">{avg_score}</div><div class="lbl">Avg Score</div></div>
</div>
</header>
""")

    # Score gauge
    score_color = "#4caf50" if avg_score >= 80 else "#ff9800" if avg_score >= 60 else "#f44336"
    circumference = 2 * 3.14159 * 50
    dash_offset = circumference * (1 - avg_score / 100)
    parts.append(f"""
<div class="score-gauge">
<svg width="120" height="120" viewBox="0 0 120 120">
  <circle cx="60" cy="60" r="50" fill="none" stroke="#1a1a2e" stroke-width="8"/>
  <circle cx="60" cy="60" r="50" fill="none" stroke="{score_color}" stroke-width="8"
    stroke-dasharray="{circumference}" stroke-dashoffset="{dash_offset}" stroke-linecap="round"/>
</svg>
<div class="score-text" style="color:{score_color}">{avg_score}</div>
</div>
""")

    # Per-screenshot reports
    for i, report in enumerate(reports):
        status_class = f"status-{report.status.lower()}"
        source_name = Path(report.source).name if report.source else f"Screenshot {i+1}"

        # Inline screenshot as base64
        img_tag = ""
        if report.source and Path(report.source).exists():
            try:
                b64 = base64.b64encode(Path(report.source).read_bytes()).decode()
                ext = Path(report.source).suffix.lower()
                mime = "image/png" if ext == ".png" else "image/jpeg" if ext in (".jpg", ".jpeg") else "image/webp"
                img_tag = f'<img src="data:{mime};base64,{b64}" alt="Screenshot">'
            except Exception:
                img_tag = '<div style="color:#666;padding:40px">Screenshot unavailable</div>'
        else:
            img_tag = '<div style="color:#666;padding:40px">No screenshot</div>'

        parts.append(f"""
<div class="report-card">
<div class="report-header">
  <span class="source">{html_module.escape(source_name)} ({report.width}x{report.height})</span>
  <span class="status-badge {status_class}">{report.status} — {report.score}/100</span>
</div>
<div class="screenshot">{img_tag}</div>
""")

        if report.glitches:
            parts.append('<div class="glitch-list">')
            for g in sorted(report.glitches, key=lambda x: {"critical": 0, "major": 1, "minor": 2, "cosmetic": 3}.get(x.severity.value if hasattr(x.severity, "value") else str(x.severity), 4)):
                sev = g.severity.value if hasattr(g.severity, "value") else str(g.severity)
                sev_class = f"sev-{sev}"
                parts.append(f"""
<div class="glitch-item">
  <div class="severity-dot {sev_class}"></div>
  <div class="glitch-detail">
    <div class="glitch-type">{html_module.escape(g.type)} — {sev.upper()}</div>
    <div class="glitch-desc">{html_module.escape(g.description)}</div>
    <div class="glitch-meta">Region: {html_module.escape(g.region)} | Confidence: {g.confidence:.0%} | Source: {html_module.escape(g.source)}</div>
  </div>
</div>""")
            parts.append('</div>')
        else:
            parts.append('<div class="no-glitches">No glitches detected</div>')

        parts.append('</div><!-- /report-card -->')

    # Footer
    parts.append(f"""
<footer>
  Generated {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | matrix_glitch_detect.py | timmy-config#544
</footer>
</div>
</body>
</html>""")

    return "\n".join(parts)


# === Output Formatting ===

def format_report(report: GlitchReport, fmt: str = "json") -> str:
    if fmt == "json":
        data = {
            "source": report.source,
            "timestamp": report.timestamp,
            "status": report.status,
            "score": report.score,
            "glitches": [asdict(g) for g in report.glitches],
            "summary": report.summary,
            "model_used": report.model_used,
        }
        for g in data["glitches"]:
            if hasattr(g["severity"], "value"):
                g["severity"] = g["severity"].value
        return json.dumps(data, indent=2)

    elif fmt == "text":
        lines = [
            "=" * 50,
            "  GLITCH DETECTION REPORT",
            "=" * 50,
            f"  Source: {report.source}",
            f"  Status: {report.status}",
            f"  Score: {report.score}/100",
            f"  Glitches: {len(report.glitches)}",
            "",
        ]
        icons = {"critical": "🔴", "major": "🟡", "minor": "🔵", "cosmetic": "⚪"}
        for g in report.glitches:
            sev = g.severity.value if hasattr(g.severity, "value") else str(g.severity)
            icon = icons.get(sev, "?")
            lines.append(f"  {icon} [{g.type}] {sev.upper()}: {g.description}")
            lines.append(f"     Region: {g.region} | Confidence: {g.confidence:.0%}")
        lines.append("")
        lines.append(f"  {report.summary}")
        lines.append("=" * 50)
        return "\n".join(lines)

    return ""


# === CLI ===

def main():
    parser = argparse.ArgumentParser(
        description="3D World Glitch Detection — visual artifact scanner for The Matrix"
    )
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("--image", help="Screenshot file to analyze")
    group.add_argument("--url", help="URL to screenshot and analyze")
    group.add_argument("--batch", help="Directory of screenshots to analyze")

    parser.add_argument("--vision", action="store_true", help="Include vision model analysis")
    parser.add_argument("--model", default=VISION_MODEL, help=f"Vision model (default: {VISION_MODEL})")
    parser.add_argument("--html", help="Generate HTML report at this path")
    parser.add_argument("--format", choices=["json", "text"], default="json", help="Output format")
    parser.add_argument("--output", "-o", help="Output file (default: stdout)")

    args = parser.parse_args()

    reports = []

    if args.image:
        print(f"Analyzing {args.image}...", file=sys.stderr)
        report = detect_glitches(args.image, args.vision, args.model)
        reports.append(report)
        if not args.html:
            print(format_report(report, args.format))

    elif args.url:
        import tempfile
        with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
            screenshot_path = tmp.name
        print(f"Capturing screenshot of {args.url}...", file=sys.stderr)
        if capture_screenshot(args.url, screenshot_path):
            report = detect_glitches(screenshot_path, args.vision, args.model)
            report.source = args.url
            reports.append(report)
            if not args.html:
                print(format_report(report, args.format))
        else:
            print(f"Failed to capture screenshot", file=sys.stderr)
            sys.exit(1)

    elif args.batch:
        batch_dir = Path(args.batch)
        images = sorted(batch_dir.glob("*.png")) + sorted(batch_dir.glob("*.jpg"))
        for img in images:
            print(f"Analyzing {img.name}...", file=sys.stderr)
            report = detect_glitches(str(img), args.vision, args.model)
            reports.append(report)

    # HTML report
    if args.html:
        html = generate_html_report(reports, title="The Matrix — Glitch Detection Report")
        Path(args.html).write_text(html)
        print(f"HTML report written to {args.html}", file=sys.stderr)
    elif args.batch and not args.html:
        # Print JSON array for batch
        print(json.dumps([json.loads(format_report(r, "json")) for r in reports], indent=2))

    # Exit code
    if any(r.status == "FAIL" for r in reports):
        sys.exit(1)


if __name__ == "__main__":
    import subprocess
    main()
