"""Cloud sync for SOAGILA unified auth.

Pushes encrypted memories to the SOAGILA cloud backend when the user is
signed in (via the VS Code `soagila.signIn` command or the Desktop app's
login screen). Auth state is read from ~/.soagila/auth.json, written by
either the VS Code extension or the Desktop app.

Key properties:
- End-to-end encrypted: content stays encrypted, server stores blobs only.
- Fire-and-forget: push failures don't block the local write.
- Auto-refresh: expired access tokens are refreshed using the refresh token.
- Tier-gated: only Pro+ tiers actually push (Free is local-only).
"""

import asyncio
import json
import logging
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

logger = logging.getLogger("soagila.cloud_sync")

API_BASE = "https://soagila-cloud-backend.sonat-ciftcioglu.workers.dev"
# API_BASE = "https://api.soagila.com"  # production

AUTH_FILE = Path.home() / ".soagila" / "auth.json"

# Tiers that allow cloud sync
SYNC_TIERS = {"pro", "enterprise", "enterprise_team", "enterprise_custom"}


def _read_auth() -> dict | None:
    """Read cached auth state from ~/.soagila/auth.json."""
    try:
        if not AUTH_FILE.exists():
            return None
        with AUTH_FILE.open("r", encoding="utf-8") as f:
            return json.load(f)
    except Exception as e:
        logger.debug(f"No auth file or parse error: {e}")
        return None


def _write_auth(state: dict) -> None:
    try:
        AUTH_FILE.parent.mkdir(parents=True, exist_ok=True)
        with AUTH_FILE.open("w", encoding="utf-8") as f:
            json.dump(state, f, indent=2)
        try:
            os.chmod(AUTH_FILE, 0o600)
        except Exception:
            pass
    except Exception as e:
        logger.warning(f"Failed to write auth file: {e}")


def is_signed_in() -> bool:
    return _read_auth() is not None


def current_user() -> dict | None:
    state = _read_auth()
    return state.get("user") if state else None


def sync_enabled() -> bool:
    """True if the signed-in user is on a tier that supports cloud sync."""
    user = current_user()
    if user is None:
        return False
    return user.get("tier") in SYNC_TIERS


async def _refresh_token() -> bool:
    """Refresh access token using the stored refresh token."""
    state = _read_auth()
    if not state or not state.get("refresh_token"):
        return False

    try:
        import aiohttp  # type: ignore
    except ImportError:
        # aiohttp not installed — fall back to requests-in-thread
        return await _refresh_token_sync()

    try:
        async with aiohttp.ClientSession() as session:
            async with session.post(
                f"{API_BASE}/auth/refresh",
                json={"refresh_token": state["refresh_token"]},
                timeout=aiohttp.ClientTimeout(total=10),
            ) as resp:
                data = await resp.json()
                if data.get("success") and data.get("data"):
                    state["access_token"] = data["data"]["access_token"]
                    if data["data"].get("refresh_token"):
                        state["refresh_token"] = data["data"]["refresh_token"]
                    if data["data"].get("user"):
                        state["user"] = data["data"]["user"]
                    state["saved_at"] = datetime.now(timezone.utc).isoformat()
                    _write_auth(state)
                    return True
    except Exception as e:
        logger.warning(f"Token refresh failed: {e}")
    return False


async def _refresh_token_sync() -> bool:
    """Refresh using urllib when aiohttp is unavailable."""
    import urllib.request
    import urllib.error

    state = _read_auth()
    if not state or not state.get("refresh_token"):
        return False

    def _do_request() -> dict | None:
        req = urllib.request.Request(
            f"{API_BASE}/auth/refresh",
            data=json.dumps({"refresh_token": state["refresh_token"]}).encode(),
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=10) as resp:
                return json.loads(resp.read().decode())
        except urllib.error.URLError:
            return None

    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(None, _do_request)
    if result and result.get("success") and result.get("data"):
        state["access_token"] = result["data"]["access_token"]
        if result["data"].get("refresh_token"):
            state["refresh_token"] = result["data"]["refresh_token"]
        if result["data"].get("user"):
            state["user"] = result["data"]["user"]
        state["saved_at"] = datetime.now(timezone.utc).isoformat()
        _write_auth(state)
        return True
    return False


async def _post_json(path: str, body: dict, retry_once: bool = True) -> dict | None:
    """Post JSON to the cloud backend with Bearer auth."""
    state = _read_auth()
    if not state:
        return None

    import urllib.request
    import urllib.error

    def _do_request(token: str) -> tuple[int, dict | None]:
        req = urllib.request.Request(
            f"{API_BASE}{path}",
            data=json.dumps(body).encode(),
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Bearer {token}",
            },
            method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=15) as resp:
                return resp.status, json.loads(resp.read().decode())
        except urllib.error.HTTPError as e:
            try:
                err_body = json.loads(e.read().decode())
            except Exception:
                err_body = None
            return e.code, err_body
        except urllib.error.URLError as e:
            logger.debug(f"Network error on {path}: {e}")
            return 0, None

    loop = asyncio.get_event_loop()
    status, result = await loop.run_in_executor(
        None, _do_request, state["access_token"]
    )

    if status == 401 and retry_once:
        # Token expired — refresh and retry once
        if await _refresh_token():
            return await _post_json(path, body, retry_once=False)

    if status == 200 and result:
        return result
    return None


async def push_memory(
    project_hash: str,
    data_type: str,
    item_id: str,
    encrypted_content: bytes | str,
    metadata: dict[str, Any],
) -> bool:
    """Push a single encrypted memory item to cloud sync.

    data_type: "episodic" | "semantic" | "receipt" | "config"
    encrypted_content: already-encrypted bytes/base64 string
    metadata: JSON-serializable summary (no PII, no content)

    Returns True on successful push, False otherwise. Non-fatal: callers
    should not raise on failure.
    """
    if not sync_enabled():
        return False

    # Base64 encode if bytes
    if isinstance(encrypted_content, (bytes, bytearray, memoryview)):
        import base64
        content_b64 = base64.b64encode(bytes(encrypted_content)).decode("ascii")
    else:
        content_b64 = encrypted_content

    body = {
        "project_hash": project_hash,
        "items": [
            {
                "data_type": data_type,
                "item_id": item_id,
                "encrypted_content": content_b64,
                "metadata": metadata,
                "version": int(datetime.now(timezone.utc).timestamp() * 1000),
            }
        ],
    }

    try:
        result = await _post_json("/sync/push", body)
        if result and result.get("success"):
            logger.debug(f"Cloud sync push OK: {data_type}/{item_id[:8]}")
            return True
    except Exception as e:
        logger.debug(f"Cloud sync push failed: {e}")

    return False


def push_memory_background(
    project_hash: str,
    data_type: str,
    item_id: str,
    encrypted_content: bytes | str,
    metadata: dict[str, Any],
) -> None:
    """Fire-and-forget cloud push. Safe to call from sync contexts."""
    if not sync_enabled():
        return

    try:
        loop = asyncio.get_event_loop()
        if loop.is_running():
            asyncio.create_task(
                push_memory(
                    project_hash, data_type, item_id, encrypted_content, metadata
                )
            )
        else:
            loop.run_until_complete(
                push_memory(
                    project_hash, data_type, item_id, encrypted_content, metadata
                )
            )
    except Exception as e:
        logger.debug(f"Background push scheduling failed: {e}")
