"""Agent tools for Hyperframes HTML-to-video rendering.

Four discoverable tools: create_video, render, lint, doctor.
All resolve the managed CLI via utils.hyperframes_manager and use
sandboxed paths under ~/.mugi/hyperframes/.
"""
from __future__ import annotations

import json
import os
import subprocess

from langchain_core.tools import tool

from tools._logging import _debug_log, _emit_input_status
from tools.live_view_visuals import emit_live_view_preview

_NOT_INSTALLED_MSG = (
    "Hyperframes is not installed. "
    "Ask the user to install it via Settings > Software > Hyperframes > Install."
)


def _with_suggested_next_step(message: str, hint: str) -> str:
    base = str(message or "").rstrip()
    step = str(hint or "").strip()
    if not step:
        return base
    if "suggested next step:" in base.lower():
        return base
    return f"{base}\nSuggested next step: {step}"


def _require_installed() -> str | None:
    """Return error string if not installed, else None."""
    from utils.hyperframes_manager import hyperframes_installed

    if not hyperframes_installed():
        return _NOT_INSTALLED_MSG
    return None


def _reject_pathlike_project_name(project_name: str) -> str | None:
    """Reject path-like inputs early; project_name must be a slug.

    The managed project root is resolved by ``resolve_project_dir``.
    Passing path fragments here is usually a loop symptom.
    """
    name = str(project_name or "").strip()
    if not name:
        return None
    if "/" in name or "\\" in name or "~" in name:
        return _with_suggested_next_step(
            (
            f"Invalid project_name {project_name!r}: use a slug only (for example `promo_video`), "
            "not a path. Hyperframes tools resolve project directories under the managed "
            "~/.mugi/hyperframes/projects root via resolve_project_dir."
            ),
            "Retry with a slug such as `promo_video` and no path separators.",
        )
    return None


def _hyperframes_project_resolution_error(project_name: str, dir_err: str | None = None) -> str:
    from persistence import HYPERFRAMES_PROJECTS_DIR

    details = f" ({dir_err})" if dir_err else ""
    return _with_suggested_next_step(
        (
        f"Project '{project_name}' not found{details}. "
        f"Use the exact slug returned by `hyperframes_create_video` under {HYPERFRAMES_PROJECTS_DIR}. "
        "Do not pass a skill name, workspace docs path (e.g. ~/Documents/Mugi/...), or other non-project identifier."
        ),
        "Call hyperframes_create_video first (or use the exact existing project slug), then retry.",
    )


@tool
def hyperframes_create_video(
    project_name: str,
    example: str = "blank",
    resolution: str = "landscape",
) -> str:
    """Create a new Hyperframes HTML video project under ~/.mugi/hyperframes/projects/<name>.

    The generated index.html wires Hyperframes timing (window.__hf). Do **not** replace it
    with a generic page: offline render forbids remote CDN <script src> and needs the template’s
    seek/duration hooks. Prefer read_file → patch → write_text_file on that path.

    Not available in plan mode (writes filesystem).

    Args:
        project_name: Slug (letters, digits, hyphens, underscores; max 64 chars).
        example: Built-in example; default "blank" is best for scripted edits.
        resolution: "landscape" (1920x1080), "portrait" (1080x1920), or "square" (1080x1080).
    """
    from utils.hyperframes_manager import (
        HYPERFRAMES_PROJECTS_DIR,
        resolve_project_dir,
        run_hyperframes,
        validate_project_name,
    )

    err = _require_installed()
    if err:
        return err

    pathlike_err = _reject_pathlike_project_name(project_name)
    if pathlike_err:
        return pathlike_err

    name_err = validate_project_name(project_name)
    if name_err:
        return name_err

    project_dir, dir_err = resolve_project_dir(project_name)
    if dir_err:
        return dir_err

    if os.path.isdir(project_dir):
        return f"Project '{project_name}' already exists at {project_dir}"

    _emit_input_status("Creating Hyperframes project…")
    emit_live_view_preview(
        f"Creating project '{project_name}'…",
        tool_name="hyperframes_create_video",
    )

    os.makedirs(HYPERFRAMES_PROJECTS_DIR, exist_ok=True)

    args = ["init", project_name, "--example", example]
    if resolution and resolution != "landscape":
        args.extend(["--resolution", resolution])

    try:
        result = run_hyperframes(
            args, cwd=HYPERFRAMES_PROJECTS_DIR, timeout_sec=120,
        )
    except subprocess.TimeoutExpired:
        return "Project creation timed out (120s)."
    except RuntimeError as exc:
        return str(exc)

    if result.returncode != 0:
        stderr = (result.stderr or "")[:500]
        return f"hyperframes init failed (exit {result.returncode}): {stderr}"

    from utils.hyperframes_manager import patch_project_for_offline_render

    patched_ok, patch_notes = patch_project_for_offline_render(project_dir)
    patch_line = ""
    if patch_notes:
        patch_line = "Offline prep: " + "; ".join(patch_notes[:6]) + "\n"
    if not patched_ok:
        return (
            f"Project created at {project_dir} but offline prep failed.\n"
            f"{patch_line}"
            "Run hyperframes_lint, fix index.html (local GSAP, window.__hf, data-start on #root), "
            "then hyperframes_render."
        )

    index_html = os.path.join(project_dir, "index.html")
    if os.path.isfile(index_html):
        size = os.path.getsize(index_html)
        abs_idx = os.path.abspath(index_html)
        return (
            f"Project created at {project_dir}\n"
            f"{patch_line}"
            f"index.html: {size} bytes ({abs_idx})\n"
            "GSAP is vendored locally and window.__hf is wired for offline render. "
            "Patch index.html in place (do not replace wholesale), then hyperframes_lint, "
            "then hyperframes_render."
        )
    return f"Project created at {project_dir} (no index.html found — check example name)."


@tool
def hyperframes_render(
    project_name: str,
    output_name: str = "output",
    format: str = "mp4",
    quality: str = "medium",
    fps: str = "30",
) -> str:
    """Render a Hyperframes project to video (MP4/WebM/MOV/GIF).

    Offline validation runs first (blocks remote <script src>, missing __hf wiring, etc.).
    Output: ~/.mugi/hyperframes/renders/<output_name>.<format>.

    Typical flow: hyperframes_create_video → read_file index.html → patch in place (no CDNs;
    keep __hf) → hyperframes_lint → hyperframes_render. Use quality "draft" for faster tries.

    Not available in plan mode (writes filesystem).

    Args:
        project_name: Existing project under ~/.mugi/hyperframes/projects/.
        output_name: Base file name (no path separators).
        format: "mp4" (default), "webm", "mov", or "gif".
        quality: "draft", "medium" (default), or "high".
        fps: Frames per second as string (default "30").
    """
    from persistence import HYPERFRAMES_RENDERS_DIR
    from utils.hyperframes_manager import (
        resolve_output_path,
        resolve_project_dir,
        run_hyperframes,
        validate_composition_offline,
    )

    err = _require_installed()
    if err:
        return err

    pathlike_err = _reject_pathlike_project_name(project_name)
    if pathlike_err:
        return pathlike_err

    project_dir, dir_err = resolve_project_dir(project_name)
    if dir_err:
        return _hyperframes_project_resolution_error(project_name, dir_err)
    if not os.path.isdir(project_dir):
        return _hyperframes_project_resolution_error(project_name)

    output_path, out_err = resolve_output_path(output_name, format)
    if out_err:
        return out_err

    _emit_input_status("Validating composition…")
    ok, warnings, errors = validate_composition_offline(project_dir)
    warning_text = ""
    if warnings:
        warning_text = "Offline validation warnings:\n" + "\n".join(f"  - {w}" for w in warnings[:10])
    if not ok:
        error_text = "Offline validation errors (render blocked):\n" + "\n".join(f"  - {e}" for e in errors[:10])
        hint = ""
        if any("missing Hyperframes timing" in e for e in errors):
            hint = (
                "\n(Fix: read_file the project index.html and restore __hf wiring from an "
                "init template; patch incrementally instead of replacing wholesale.)"
            )
        elif any("remote <script src>" in e for e in errors):
            hint = "\n(Fix: remove CDN script tags or vendor files under the project directory.)"
        return f"{error_text}{hint}\n{warning_text}".strip()

    os.makedirs(HYPERFRAMES_RENDERS_DIR, exist_ok=True)

    _emit_input_status("Rendering video…")
    emit_live_view_preview(
        f"Rendering '{project_name}'…",
        tool_name="hyperframes_render",
        progress_pct=0,
    )

    args = [
        "render",
        "--output", output_path,
        "--format", format,
        "--quality", quality,
        "--fps", fps,
        "--workers", "1",
    ]

    try:
        result = run_hyperframes(
            args, cwd=project_dir, timeout_sec=300, use_render_lock=True,
        )
    except subprocess.TimeoutExpired:
        return "Render timed out (300s). Try --quality draft or lower fps for faster iteration."
    except RuntimeError as exc:
        return str(exc)

    if result.returncode != 0:
        stderr = (result.stderr or "")[:500]
        combined = stderr
        suffix = ""
        if "__hf" in combined.lower() or "frameready" in combined.lower():
            suffix = (
                " Composition hint: Hyperframes expects window.__hf (duration/seek) from the "
                "init template—patch index.html incrementally instead of replacing with plain HTML.\n"
            )
        elif "requestAnimationFrame" in combined.lower():
            suffix = (
                " If screenshot capture timed out, avoid raw requestAnimationFrame loops without "
                "the template’s Hyperframes lifecycle, or simplify motion to seek-driven timelines.\n"
            )
        return (
            f"Render failed (exit {result.returncode}): {stderr}\n"
            f"{suffix}"
            "Run hyperframes_lint on this project_name, then hyperframes_doctor if needed."
        )

    emit_live_view_preview(
        "Render complete.",
        tool_name="hyperframes_render",
        progress_pct=100,
    )

    file_size = 0
    if os.path.isfile(output_path):
        file_size = os.path.getsize(output_path)

    parts = [f"Rendered to {output_path} ({file_size:,} bytes)"]
    if warning_text:
        parts.append(warning_text)
    return "\n".join(parts)


@tool
def hyperframes_lint(project_name: str) -> str:
    """Lint a Hyperframes composition (run before hyperframes_render when debugging).

    Runs offline render preflight (remote scripts, missing __hf wiring) plus the
    Hyperframes CLI linter. Treat ``errorCount > 0`` or ``ok: false`` as blocking.

    Args:
        project_name: Project under ~/.mugi/hyperframes/projects/.
    """
    from utils.hyperframes_manager import (
        resolve_project_dir,
        run_hyperframes,
        validate_composition_offline,
    )

    err = _require_installed()
    if err:
        return err

    pathlike_err = _reject_pathlike_project_name(project_name)
    if pathlike_err:
        return pathlike_err

    project_dir, dir_err = resolve_project_dir(project_name)
    if dir_err:
        return _hyperframes_project_resolution_error(project_name, dir_err)
    if not os.path.isdir(project_dir):
        return _hyperframes_project_resolution_error(project_name)

    findings: list[dict[str, str]] = []
    offline_ok, offline_warnings, offline_errors = validate_composition_offline(project_dir)
    for msg in offline_errors:
        findings.append(
            {
                "code": "offline_render_blocked",
                "severity": "error",
                "message": msg,
            }
        )
    for msg in offline_warnings:
        findings.append(
            {
                "code": "offline_render_warning",
                "severity": "warning",
                "message": msg,
            }
        )

    _emit_input_status("Linting project…")

    cli_ok = True
    try:
        result = run_hyperframes(
            ["lint", "--json"], cwd=project_dir, timeout_sec=30,
        )
    except subprocess.TimeoutExpired:
        findings.append(
            {
                "code": "lint_timeout",
                "severity": "error",
                "message": "Lint timed out (30s).",
            }
        )
        cli_ok = False
        result = None
    except RuntimeError as exc:
        findings.append(
            {
                "code": "lint_runtime_error",
                "severity": "error",
                "message": str(exc),
            }
        )
        cli_ok = False
        result = None

    if result is not None:
        stdout = (result.stdout or "").strip()
        if stdout:
            try:
                data = json.loads(stdout)
                issues = data if isinstance(data, list) else data.get("issues", data)
                if isinstance(issues, list):
                    for item in issues:
                        if isinstance(item, dict):
                            findings.append(
                                {
                                    "code": str(item.get("code") or item.get("rule") or "cli_lint"),
                                    "severity": str(item.get("severity") or "warning"),
                                    "message": str(item.get("message") or item),
                                }
                            )
                        else:
                            findings.append(
                                {
                                    "code": "cli_lint",
                                    "severity": "warning",
                                    "message": str(item),
                                }
                            )
                elif isinstance(data, dict) and data.get("findings"):
                    for item in data.get("findings") or []:
                        if isinstance(item, dict):
                            findings.append(
                                {
                                    "code": str(item.get("code") or "cli_lint"),
                                    "severity": str(item.get("severity") or "warning"),
                                    "message": str(item.get("message") or item),
                                }
                            )
            except json.JSONDecodeError:
                if stdout:
                    findings.append(
                        {
                            "code": "cli_lint_output",
                            "severity": "info",
                            "message": stdout[:500],
                        }
                    )
        if result.returncode != 0:
            cli_ok = False
            stderr = (result.stderr or "").strip()
            if stderr:
                findings.append(
                    {
                        "code": "cli_lint_failed",
                        "severity": "error",
                        "message": stderr[:500],
                    }
                )

    error_count = sum(1 for f in findings if f.get("severity") == "error")
    warning_count = sum(1 for f in findings if f.get("severity") == "warning")
    info_count = sum(1 for f in findings if f.get("severity") not in ("error", "warning"))
    payload = {
        "ok": offline_ok and cli_ok and error_count == 0,
        "errorCount": error_count,
        "warningCount": warning_count,
        "infoCount": info_count,
        "findings": findings,
    }
    return json.dumps(payload, indent=2)


@tool
def hyperframes_doctor() -> str:
    """Run Hyperframes diagnostics.

    Checks Node.js, FFmpeg, FFprobe, and Chromium availability.
    Returns a structured pass/fail report.
    """
    from utils.hyperframes_manager import doctor_hyperframes, hyperframes_installed

    if not hyperframes_installed():
        return _NOT_INSTALLED_MSG

    _emit_input_status("Running Hyperframes diagnostics…")

    result = doctor_hyperframes()
    if not result:
        return "Doctor returned no output."

    if result.get("ok") is True:
        return "All checks passed.\n" + json.dumps(result, indent=2)

    error = result.get("error", "")
    return f"Issues detected: {error}\n" + json.dumps(result, indent=2)


def _register_hyperframes_managed_root() -> None:
    try:
        from persistence import HYPERFRAMES_PROJECTS_DIR
        from utils.managed_path_roots import register_managed_root

        register_managed_root(HYPERFRAMES_PROJECTS_DIR, "Hyperframes project slug")
    except Exception:
        pass


_register_hyperframes_managed_root()
