"""
Apps Script integration helpers for g-gremlin.

Provides pull/push/run support for container-bound script projects.
"""

from __future__ import annotations

import json
import os
import socket
import time
from pathlib import Path
from typing import Dict, List, Optional

from googleapiclient.errors import HttpError

from g_gremlin import google_client
from g_gremlin.paths import gremlin_path

SCRIPT_BINDINGS_PATH = gremlin_path("script_bindings.json")


class ScriptResolutionError(Exception):
    """Raised when a script id cannot be resolved automatically."""


def _resolve_timeout(timeout_sec: Optional[int]) -> int:
    if timeout_sec is not None:
        return max(1, int(timeout_sec))
    raw = os.getenv("G_GREMLIN_APPS_SCRIPT_TIMEOUT_SEC", "300")
    try:
        return max(1, int(raw))
    except (TypeError, ValueError):
        return 300


def _resolve_retries(retries: Optional[int]) -> int:
    if retries is not None:
        return max(1, int(retries))
    raw = os.getenv("G_GREMLIN_APPS_SCRIPT_RETRIES", "2")
    try:
        return max(1, int(raw))
    except (TypeError, ValueError):
        return 2


def _is_retryable_http_error(exc: HttpError) -> bool:
    status = getattr(getattr(exc, "resp", None), "status", None)
    return status in (429, 500, 502, 503, 504)


def _is_timeout_like(exc: Exception) -> bool:
    if isinstance(exc, (TimeoutError, socket.timeout)):
        return True
    return "timed out" in str(exc).lower()


def _backoff_sleep(attempt: int) -> None:
    time.sleep(min(1.5, 0.25 * max(1, attempt)))


def _set_script_http_timeout(service, timeout_sec: int) -> None:
    """Best-effort timeout tuning for googleapiclient's underlying HTTP transport."""
    http = getattr(service, "_http", None)
    if http is None:
        return
    for target in (http, getattr(http, "http", None)):
        if target is None:
            continue
        try:
            target.timeout = timeout_sec
        except Exception:
            # Keep best-effort behavior; transport internals vary by runtime.
            pass


def _is_private_call_blocked(exc: HttpError, function_name: str) -> bool:
    """Detect Execution API refusal for private helper names."""
    if not str(function_name or "").endswith("_"):
        return False
    status = getattr(getattr(exc, "resp", None), "status", None)
    if status not in (400, 403):
        return False
    payload = getattr(exc, "content", b"")
    if isinstance(payload, (bytes, bytearray)):
        message = payload.decode("utf-8", errors="ignore").lower()
    else:
        message = str(payload).lower()
    markers = (
        "forbidden",
        "permission",
        "cannot be called",
        "not found",
        "script function",
    )
    return any(marker in message for marker in markers)


def _load_bindings() -> Dict[str, str]:
    if not SCRIPT_BINDINGS_PATH.exists():
        return {}
    try:
        return json.loads(SCRIPT_BINDINGS_PATH.read_text(encoding="utf-8")) or {}
    except Exception:
        return {}


def _save_bindings(bindings: Dict[str, str]) -> None:
    SCRIPT_BINDINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
    SCRIPT_BINDINGS_PATH.write_text(json.dumps(bindings, indent=2), encoding="utf-8")


def cache_binding(spreadsheet_id: str, script_id: str) -> None:
    bindings = _load_bindings()
    bindings[spreadsheet_id] = script_id
    _save_bindings(bindings)


def resolve_script_id(
    spreadsheet_id: str,
    explicit_script_id: Optional[str] = None,
    create_if_missing: bool = False,
    title: Optional[str] = None,
) -> str:
    """
    Resolve the Apps Script project ID for a spreadsheet.

    Preference order:
    1) Explicit --script-id parameter
    2) Local binding cache (~/.g_gremlin/script_bindings.json)
    3) Drive lookup for container-bound script
    4) Optional create (when create_if_missing=True)
    """
    if explicit_script_id:
        return explicit_script_id

    bindings = _load_bindings()
    if spreadsheet_id in bindings:
        return bindings[spreadsheet_id]

    script_id = find_bound_script(spreadsheet_id)
    if script_id:
        cache_binding(spreadsheet_id, script_id)
        return script_id

    if create_if_missing:
        created = create_bound_script(spreadsheet_id, title=title)
        cache_binding(spreadsheet_id, created)
        return created

    raise ScriptResolutionError(
        "No bound Apps Script project found. Provide --script-id or run 'g-gremlin script init'."
    )


def find_bound_script(spreadsheet_id: str) -> Optional[str]:
    """
    Attempt to locate a container-bound script via Drive metadata.
    """
    drive = google_client.get_drive_service()
    query = (
        "mimeType='application/vnd.google-apps.script' "
        "and trashed=false "
        f"and '{spreadsheet_id}' in parents"
    )
    resp = drive.files().list(q=query, fields="files(id,name,parents)").execute()
    files = resp.get("files", []) or []
    if not files:
        return None
    return files[0].get("id")


def create_bound_script(spreadsheet_id: str, title: Optional[str] = None) -> str:
    """
    Create a new container-bound Apps Script project for a spreadsheet.
    """
    service = google_client.get_script_service()
    body = {
        "title": title or f"Gremlin Script ({spreadsheet_id[:6]})",
        "parentId": spreadsheet_id,
    }
    resp = service.projects().create(body=body).execute()
    script_id = resp.get("scriptId")
    if not script_id:
        raise RuntimeError("Failed to create Apps Script project (no scriptId returned).")
    return script_id


def pull_script(script_id: str, output_dir: Path) -> List[Path]:
    """
    Download an Apps Script project to a local directory.
    """
    service = google_client.get_script_service()
    content = service.projects().getContent(scriptId=script_id).execute()
    files = content.get("files", []) or []
    output_dir.mkdir(parents=True, exist_ok=True)

    written: List[Path] = []
    for file in files:
        name = file.get("name", "Code")
        ftype = file.get("type", "SERVER_JS")
        source = file.get("source", "")

        if ftype == "JSON":
            dest = output_dir / "appsscript.json"
        elif ftype == "HTML":
            dest = output_dir / f"{name}.html"
        else:
            dest = output_dir / f"{name}.gs"

        dest.write_text(source, encoding="utf-8")
        written.append(dest)

    return written


def push_script(script_id: str, source_dir: Path) -> int:
    """
    Upload local script files to the Apps Script project (full replace).
    """
    if not source_dir.exists():
        raise FileNotFoundError(f"Script source directory not found: {source_dir}")

    files = _collect_local_files(source_dir)
    if not files:
        raise ValueError(f"No .gs/.html or appsscript.json files found in {source_dir}")

    service = google_client.get_script_service()
    service.projects().updateContent(scriptId=script_id, body={"files": files}).execute()
    return len(files)


def run_function(
    script_id: str,
    function_name: str,
    params: Optional[List] = None,
    timeout_sec: Optional[int] = None,
    retries: Optional[int] = None,
) -> Dict:
    """
    Execute an Apps Script function via the Execution API.

    If a private helper (function name ending with "_") is blocked by the
    Execution API, retry through qaInvokePrivate(function_name, params).
    """
    service = google_client.get_script_service()
    timeout = _resolve_timeout(timeout_sec)
    attempts = _resolve_retries(retries)
    _set_script_http_timeout(service, timeout)
    args = params or []
    body: Dict[str, object] = {
        "function": function_name,
        "parameters": args,
        "devMode": True,
    }
    for attempt in range(1, attempts + 1):
        try:
            return service.scripts().run(scriptId=script_id, body=body).execute()
        except HttpError as exc:
            if _is_private_call_blocked(exc, function_name):
                proxy_body = {
                    "function": "qaInvokePrivate",
                    "parameters": [function_name, args],
                    "devMode": True,
                }
                return service.scripts().run(scriptId=script_id, body=proxy_body).execute()
            if attempt < attempts and _is_retryable_http_error(exc):
                _backoff_sleep(attempt)
                continue
            raise
        except Exception as exc:
            if attempt < attempts and _is_timeout_like(exc):
                _backoff_sleep(attempt)
                continue
            raise
    raise RuntimeError("Apps Script execution retry loop exhausted unexpectedly")


def list_recent_operations(script_id: str, page_size: int = 10) -> List[Dict]:
    """
    List recent Apps Script executions (best-effort).
    """
    service = google_client.get_script_service()
    resp = (
        service.projects()
        .operations()
        .list(scriptId=script_id, pageSize=page_size)
        .execute()
    )
    return resp.get("operations", []) or []


def _collect_local_files(source_dir: Path) -> List[Dict]:
    files: List[Dict] = []

    manifest = source_dir / "appsscript.json"
    if manifest.exists():
        files.append(
            {
                "name": "appsscript",
                "type": "JSON",
                "source": manifest.read_text(encoding="utf-8"),
            }
        )

    for path in sorted(source_dir.glob("*.gs")):
        files.append(
            {"name": path.stem, "type": "SERVER_JS", "source": path.read_text(encoding="utf-8")}
        )

    for path in sorted(source_dir.glob("*.html")):
        files.append(
            {"name": path.stem, "type": "HTML", "source": path.read_text(encoding="utf-8")}
        )

    return files
