"""Plugin routes — list, install, activate, deactivate, uninstall, manifest schema, config, upload, install-url."""

from __future__ import annotations

import importlib
import logging
import os
import re
import shutil
import tempfile
import zipfile
from pathlib import Path
from typing import Any

import httpx
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.db import get_db
from app.deps import require_permission
from app.plugins.base import BasePlugin
from app.plugins.manifest import PluginManifest
from app.plugins.migration_runner import MigrationValidationError
from app.services.plugin_service import get_plugin_service

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/v1/plugins", tags=["plugins"])


class PluginConfigUpdate(BaseModel):
    """Request body for updating plugin configuration."""
    config: dict


class PluginUrlInstall(BaseModel):
    """Request body for installing a plugin from a URL."""
    url: str


MAX_UPLOAD_SIZE = 10 * 1024 * 1024  # 10 MB


@router.get("")
async def list_plugins(
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("plugins:read")),
):
    """List all plugins with their current status (discovered, installed, active, inactive)."""
    service = get_plugin_service()
    plugins = await service.list_plugins(db)
    return {"plugins": plugins, "total": len(plugins)}


@router.get("/manifest")
async def get_manifest_schema(
    current_user: dict = Depends(require_permission("plugins:read")),
):
    """Get the plugin manifest schema documentation."""
    service = get_plugin_service()
    return service.get_manifest_schema()


@router.get("/active-manifests")
async def get_active_manifests(
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("plugins:read")),
):
    """Get UI manifests for all active plugins.

    Returns menu_items, page_routes, detail_tabs, settings_pages, and
    dashboard_widgets contributed by each active plugin. Used by the
    frontend PluginRegistry to dynamically register routes, sidebar items,
    settings pages, and detail tabs.
    """
    service = get_plugin_service()
    manifests = await service.get_active_manifests(db)
    return {"plugins": manifests, "total": len(manifests)}


@router.get("/{name}/config")
async def get_plugin_config(
    name: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("plugins:read")),
):
    """Get the configuration for a specific plugin.

    Returns the plugin's config field as a JSON object.
    """
    service = get_plugin_service()
    try:
        config = await service.get_plugin_config(db, name)
        return {"name": name, "config": config}
    except ValueError as exc:
        raise HTTPException(
            404, detail={"detail": str(exc), "code": "plugin_not_found"}
        ) from None


@router.patch("/{name}/config")
async def update_plugin_config(
    name: str,
    body: PluginConfigUpdate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("plugins:configure")),
):
    """Update the configuration for a specific plugin.

    Stores the config as a JSON string in the plugin's config field.
    """
    import uuid as uuid_mod

    service = get_plugin_service()
    try:
        result = await service.update_plugin_config(
            db,
            name,
            config=body.config,
            tenant_id=uuid_mod.UUID(current_user["tenant_id"]),
            user_id=uuid_mod.UUID(current_user["user_id"]),
        )
        return result
    except ValueError as exc:
        raise HTTPException(
            404, detail={"detail": str(exc), "code": "plugin_not_found"}
        ) from None


@router.post("/{name}/install")
async def install_plugin(
    name: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("plugins:configure")),
):
    """Install a plugin by name. Runs migrations and creates DB record.

    Idempotent: returns 200 if already installed.
    """
    import uuid as uuid_mod

    service = get_plugin_service()
    try:
        result = await service.install_plugin(
            db,
            name,
            tenant_id=uuid_mod.UUID(current_user["tenant_id"]),
            user_id=uuid_mod.UUID(current_user["user_id"]),
        )
        return result
    except ValueError as exc:
        if "not found" in str(exc).lower():
            raise HTTPException(
                404, detail={"detail": str(exc), "code": "plugin_not_found"}
            ) from None
        raise HTTPException(400, detail={"detail": str(exc), "code": "plugin_error"}) from None
    except MigrationValidationError as exc:
        raise HTTPException(
            422, detail={"detail": str(exc), "code": "migration_validation_error"}
        ) from None


@router.post("/{name}/activate")
async def activate_plugin(
    name: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("plugins:configure")),
):
    """Activate a plugin by name. Registers event listeners and routes.

    Idempotent: returns 200 if already active.
    """
    import uuid as uuid_mod

    service = get_plugin_service()
    try:
        result = await service.activate_plugin(
            db,
            name,
            tenant_id=uuid_mod.UUID(current_user["tenant_id"]),
            user_id=uuid_mod.UUID(current_user["user_id"]),
        )
        return result
    except ValueError as exc:
        if "not installed" in str(exc).lower():
            raise HTTPException(
                400, detail={"detail": str(exc), "code": "plugin_not_installed"}
            ) from None
        if "not found" in str(exc).lower():
            raise HTTPException(
                404, detail={"detail": str(exc), "code": "plugin_not_found"}
            ) from None
        raise HTTPException(400, detail={"detail": str(exc), "code": "plugin_error"}) from None


@router.post("/{name}/deactivate")
async def deactivate_plugin(
    name: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("plugins:configure")),
):
    """Deactivate a plugin by name. Unregisters event listeners and routes.

    Idempotent: returns 200 if already inactive.
    """
    import uuid as uuid_mod

    service = get_plugin_service()
    try:
        result = await service.deactivate_plugin(
            db,
            name,
            tenant_id=uuid_mod.UUID(current_user["tenant_id"]),
            user_id=uuid_mod.UUID(current_user["user_id"]),
        )
        return result
    except ValueError as exc:
        if "not found" in str(exc).lower():
            raise HTTPException(
                404, detail={"detail": str(exc), "code": "plugin_not_found"}
            ) from None
        raise HTTPException(400, detail={"detail": str(exc), "code": "plugin_error"}) from None


@router.delete("/{name}")
async def uninstall_plugin(
    name: str,
    remove_data: bool = Query(False),
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("plugins:configure")),
):
    """Uninstall a plugin by name. Deactivates, drops tables, removes DB record.

    Idempotent: returns 200 if already uninstalled.
    """
    import uuid as uuid_mod

    service = get_plugin_service()
    try:
        result = await service.uninstall_plugin(
            db,
            name,
            remove_data=remove_data,
            tenant_id=uuid_mod.UUID(current_user["tenant_id"]),
            user_id=uuid_mod.UUID(current_user["user_id"]),
        )
        return result
    except ValueError as exc:
        if "not found" in str(exc).lower():
            raise HTTPException(
                404, detail={"detail": str(exc), "code": "plugin_not_found"}
            ) from None
        raise HTTPException(400, detail={"detail": str(exc), "code": "plugin_error"}) from None


# ── Plugin Upload / URL Install ──────────────────────────────────────────────


def _validate_manifest_name(name: str) -> str:
    """Validate plugin name is alphanumeric with underscores only."""
    if not re.match(r"^[a-zA-Z][a-zA-Z0-9_]*$", name):
        raise ValueError(
            f"Invalid plugin name '{name}': must start with a letter and contain only "
            f"alphanumeric characters and underscores"
        )
    return name


def _check_dangerous_imports(source_code: str) -> list[str]:
    """Check plugin source for dangerous imports/patterns.

    Returns a list of dangerous patterns found (empty if safe).
    """
    dangerous_patterns = [
        (r"\bos\.system\b", "os.system call"),
        (r"\bsubprocess\.", "subprocess module"),
        (r"\beval\s*\(", "eval() call"),
        (r"\bexec\s*\(", "exec() call"),
        (r"\b__import__\s*\(", "__import__() call"),
        (r"\bcompile\s*\(", "compile() call"),
    ]
    found: list[str] = []
    for pattern, description in dangerous_patterns:
        if re.search(pattern, source_code):
            found.append(description)
    return found


def _check_migration_sql(sql_content: str) -> list[str]:
    """Basic SQL validation for migration files.

    Returns a list of issues found (empty if OK).
    """
    issues: list[str] = []
    # Check for basic SQL syntax issues
    lines = sql_content.strip().split("\n")
    for i, line in enumerate(lines, 1):
        stripped = line.strip()
        if not stripped or stripped.startswith("--"):
            continue
        # Check for unclosed parentheses
        if stripped.count("(") != stripped.count(")"):
            issues.append(f"Line {i}: unbalanced parentheses")
        # Check for DROP TABLE (dangerous in migrations)
        if re.search(r"\bDROP\s+TABLE\b", stripped, re.IGNORECASE):
            issues.append(f"Line {i}: DROP TABLE is not allowed in plugin migrations")
    return issues


def _find_plugin_class_in_module(module: Any) -> type[BasePlugin] | None:
    """Find a BasePlugin subclass in a module."""
    for attr_name in dir(module):
        attr = getattr(module, attr_name)
        if (
            isinstance(attr, type)
            and issubclass(attr, BasePlugin)
            and attr is not BasePlugin
        ):
            return attr
    return None


def _extract_plugin_from_zip(zip_path: str) -> tuple[Path, str, type[BasePlugin]]:
    """Extract a ZIP file and find the plugin class.

    Returns (extract_dir, plugin_name, plugin_class).
    """
    extract_dir = Path(tempfile.mkdtemp(prefix="plugin_upload_"))
    try:
        with zipfile.ZipFile(zip_path, "r") as zf:
            # Validate ZIP
            bad_files = [f for f in zf.namelist() if f.startswith("..") or f.startswith("/")]
            if bad_files:
                raise ValueError(f"ZIP contains files with unsafe paths: {bad_files}")
            zf.extractall(extract_dir)

        # Find plugin.py in the extracted contents
        plugin_py_path: Path | None = None
        for fpath in extract_dir.rglob("plugin.py"):
            plugin_py_path = fpath
            break

        if plugin_py_path is None:
            raise ValueError("ZIP does not contain a plugin.py file")

        # Import the module dynamically
        spec = importlib.util.spec_from_file_location(
            "uploaded_plugin", plugin_py_path
        )
        if spec is None or spec.loader is None:
            raise ValueError("Could not load plugin.py module")
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)

        # Find BasePlugin subclass
        plugin_class = _find_plugin_class_in_module(module)
        if plugin_class is None:
            raise ValueError(
                "plugin.py does not contain a BasePlugin subclass"
            )

        plugin_instance = plugin_class()
        plugin_name = plugin_instance.name

        # Validate manifest
        manifest = plugin_instance.manifest
        if not manifest.name or not manifest.version or not manifest.display_name:
            raise ValueError(
                "Plugin manifest must include name, version, and display_name"
            )

        # Validate plugin name
        _validate_manifest_name(manifest.name)

        # Check for dangerous imports in plugin.py
        source_code = plugin_py_path.read_text(encoding="utf-8")
        dangerous = _check_dangerous_imports(source_code)
        if dangerous:
            raise ValueError(
                f"Plugin contains dangerous patterns: {', '.join(dangerous)}"
            )

        # Check migration SQL files
        migrations_dir = plugin_py_path.parent / "migrations"
        if migrations_dir.exists():
            for sql_file in sorted(migrations_dir.glob("*.sql")):
                sql_content = sql_file.read_text(encoding="utf-8")
                issues = _check_migration_sql(sql_content)
                if issues:
                    raise ValueError(
                        f"Migration file {sql_file.name} has issues: {'; '.join(issues)}"
                    )

        return extract_dir, plugin_name, plugin_class

    except Exception:
        # Clean up on failure
        shutil.rmtree(extract_dir, ignore_errors=True)
        raise


def _install_plugin_from_dir(
    extract_dir: Path,
    plugin_name: str,
    plugin_class: type[BasePlugin],
) -> None:
    """Copy plugin directory to builtins and register it.

    Copies the extracted plugin directory to app/plugins/builtins/{plugin_name}/.
    """
    builtins_dir = Path(__file__).parent.parent / "plugins" / "builtins" / plugin_name
    builtins_dir.mkdir(parents=True, exist_ok=True)

    # Copy all files from extract_dir to builtins_dir
    for item in extract_dir.iterdir():
        dest = builtins_dir / item.name
        if item.is_dir():
            if dest.exists():
                shutil.rmtree(dest)
            shutil.copytree(item, dest)
        else:
            shutil.copy2(item, dest)

    # Register the plugin in the registry
    registry = get_plugin_service().registry
    instance = plugin_class()
    registry.register_plugin(instance)

    logger.info(
        "Installed plugin '%s' from uploaded ZIP to %s",
        plugin_name,
        builtins_dir,
    )


@router.post("/upload")
async def upload_plugin(
    file: UploadFile = File(...),
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("plugins:configure")),
):
    """Upload and install a plugin from a ZIP file.

    The ZIP must contain a plugin directory with a plugin.py that defines a BasePlugin subclass.
    Validates the manifest, checks for conflicts, runs migrations, and installs the plugin.
    """
    import uuid as uuid_mod

    # Validate file is a ZIP
    if not file.filename or not file.filename.endswith(".zip"):
        raise HTTPException(400, detail={"detail": "File must be a .zip archive", "code": "invalid_file"})

    # Check file size
    contents = await file.read()
    if len(contents) > MAX_UPLOAD_SIZE:
        raise HTTPException(
            413,
            detail={
                "detail": f"File too large. Maximum size is {MAX_UPLOAD_SIZE // (1024*1024)} MB",
                "code": "file_too_large",
            },
        )

    # Write to temp file
    tmp_zip = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
    try:
        tmp_zip.write(contents)
        tmp_zip.close()

        # Extract and validate
        extract_dir, plugin_name, plugin_class = _extract_plugin_from_zip(tmp_zip.name)

        # Check for name conflicts with existing plugins
        service = get_plugin_service()
        existing_plugins = await service.list_plugins(db)
        existing_names = {p["name"] for p in existing_plugins}

        if plugin_name in existing_names:
            # Check if version is higher
            existing_plugin = next(
                (p for p in existing_plugins if p["name"] == plugin_name), None
            )
            if existing_plugin:
                raise HTTPException(
                    409,
                    detail={
                        "detail": f"Plugin '{plugin_name}' already exists (version {existing_plugin.get('version', 'unknown')}). "
                        f"Uninstall the existing plugin first or upload a higher version.",
                        "code": "plugin_exists",
                    },
                )

        # Install the plugin directory
        _install_plugin_from_dir(extract_dir, plugin_name, plugin_class)

        # Run migrations and install via service
        result = await service.install_plugin(
            db,
            plugin_name,
            tenant_id=uuid_mod.UUID(current_user["tenant_id"]),
            user_id=uuid_mod.UUID(current_user["user_id"]),
        )

        # Log audit
        from app.core.audit import log_audit
        await log_audit(
            db,
            uuid_mod.UUID(current_user["tenant_id"]),
            uuid_mod.UUID(current_user["user_id"]),
            action="plugin.upload",
            entity_type="plugin",
            changes={"name": plugin_name, "version": result.get("version"), "method": "upload"},
        )

        return {
            **result,
            "message": f"Plugin '{plugin_name}' uploaded and installed successfully",
        }

    except ValueError as exc:
        raise HTTPException(400, detail={"detail": str(exc), "code": "plugin_validation_error"}) from None
    except MigrationValidationError as exc:
        raise HTTPException(
            422, detail={"detail": str(exc), "code": "migration_validation_error"}
        ) from None
    except Exception as exc:
        logger.exception("Failed to upload plugin")
        raise HTTPException(
            500, detail={"detail": f"Failed to install plugin: {str(exc)}", "code": "install_error"}
        ) from None
    finally:
        # Clean up temp files
        try:
            os.unlink(tmp_zip.name)
        except Exception:
            pass
        try:
            if "extract_dir" in dir():
                shutil.rmtree(extract_dir, ignore_errors=True)
        except Exception:
            pass


@router.post("/install-url")
async def install_plugin_from_url(
    body: PluginUrlInstall,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("plugins:configure")),
):
    """Install a plugin from a URL (downloads ZIP and installs)."""
    import uuid as uuid_mod

    if not body.url:
        raise HTTPException(400, detail={"detail": "URL is required", "code": "missing_url"})

    # Download ZIP from URL
    tmp_zip = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
    try:
        async with httpx.AsyncClient(timeout=60.0) as client:
            response = await client.get(body.url, follow_redirects=True)
            response.raise_for_status()

            content = response.content
            if len(content) > MAX_UPLOAD_SIZE:
                raise HTTPException(
                    413,
                    detail={
                        "detail": f"Downloaded file too large. Maximum size is {MAX_UPLOAD_SIZE // (1024*1024)} MB",
                        "code": "file_too_large",
                    },
                )

            tmp_zip.write(content)
            tmp_zip.close()

        # Extract and validate
        extract_dir, plugin_name, plugin_class = _extract_plugin_from_zip(tmp_zip.name)

        # Check for name conflicts
        service = get_plugin_service()
        existing_plugins = await service.list_plugins(db)
        existing_names = {p["name"] for p in existing_plugins}

        if plugin_name in existing_names:
            raise HTTPException(
                409,
                detail={
                    "detail": f"Plugin '{plugin_name}' already exists. Uninstall the existing plugin first.",
                    "code": "plugin_exists",
                },
            )

        # Install the plugin directory
        _install_plugin_from_dir(extract_dir, plugin_name, plugin_class)

        # Run migrations and install via service
        result = await service.install_plugin(
            db,
            plugin_name,
            tenant_id=uuid_mod.UUID(current_user["tenant_id"]),
            user_id=uuid_mod.UUID(current_user["user_id"]),
        )

        # Log audit
        from app.core.audit import log_audit
        await log_audit(
            db,
            uuid_mod.UUID(current_user["tenant_id"]),
            uuid_mod.UUID(current_user["user_id"]),
            action="plugin.install_url",
            entity_type="plugin",
            changes={"name": plugin_name, "version": result.get("version"), "url": body.url},
        )

        return {
            **result,
            "message": f"Plugin '{plugin_name}' downloaded and installed successfully",
        }

    except httpx.HTTPStatusError as exc:
        raise HTTPException(
            400,
            detail={
                "detail": f"Failed to download plugin from URL: HTTP {exc.response.status_code}",
                "code": "download_error",
            },
        ) from None
    except httpx.RequestError as exc:
        raise HTTPException(
            400,
            detail={
                "detail": f"Failed to download plugin from URL: {str(exc)}",
                "code": "download_error",
            },
        ) from None
    except ValueError as exc:
        raise HTTPException(400, detail={"detail": str(exc), "code": "plugin_validation_error"}) from None
    except MigrationValidationError as exc:
        raise HTTPException(
            422, detail={"detail": str(exc), "code": "migration_validation_error"}
        ) from None
    except Exception as exc:
        logger.exception("Failed to install plugin from URL")
        raise HTTPException(
            500, detail={"detail": f"Failed to install plugin: {str(exc)}", "code": "install_error"}
        ) from None
    finally:
        # Clean up temp files
        try:
            os.unlink(tmp_zip.name)
        except Exception:
            pass
        try:
            if "extract_dir" in dir():
                shutil.rmtree(extract_dir, ignore_errors=True)
        except Exception:
            pass
