"""Embedded Rclone process management for multi-machine sync.

Replaces manual rclone setup with automatic download + configuration.
Rclone is auto-downloaded on first use if not already available.

Sync flow:
1. rclone installed/found → configured with remote
2. Push: copy local DB to remote folder
3. Pull: copy remote DB to local, merge via JSONL export/import
4. Auto-sync: periodic push/pull in background

Resilience:
- Auto-download rclone binary on first use
- Health check before sync operations
- Conflict resolution via timestamp-based merge
- Configurable sync interval (0 = manual only)
"""

from __future__ import annotations

import asyncio
import base64
import hashlib
import json
import os
import platform
import shutil
import stat
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path
from typing import TYPE_CHECKING

import httpx
from loguru import logger

from mnemo_mcp.config import settings

if TYPE_CHECKING:
    from mnemo_mcp.db import MemoryDB


# Expected SHA256 checksums for rclone archives (v1.68.2)
_RCLONE_CHECKSUMS = {
    "linux-amd64": "0e6fa18051e67fc600d803a2dcb10ddedb092247fc6eee61be97f64ec080a13c",
    "linux-arm64": "c6e9d4cf9c88b279f6ad80cd5675daebc068e404890fa7e191412c1bc7a4ac5f",
    "linux-386": "8654f19f572ac90c8cf712f3e212ee499b8e5e270e209753f3e82f0b44d9447d",
    "osx-amd64": "cdc685e16abbf35b6f47c95b2a5b4ad73a73921ff6842e5f4136c8b461756188",
    "osx-arm64": "323f387b32bcf9ddfc3874f01879a0b2689dbd91309beb8c3a4410db04d0c41f",
    "windows-amd64": "812bf76cc02c04cf6327f3683f3d5a88e47d36c39db84c1a745777496be7d993",
    "windows-arm64": "cbc6584266cf62bb9f4df912cb00d566c1cbc50ce2748f5e433f1937209e807e",
    "windows-386": "d076d341122287cf92033aeecf1dd6900ff407c22981fa5ddf49689d5301a7e2",
}

# Background sync task reference
_sync_task: asyncio.Task | None = None


def _get_rclone_dir() -> Path:
    """Get directory for rclone binary."""
    return settings.get_data_dir() / "bin"


def _get_rclone_path() -> Path | None:
    """Find rclone binary.

    Priority:
    1. System-installed rclone (in PATH)
    2. Bundled rclone in data dir
    """
    # Check system PATH first
    system_rclone = shutil.which("rclone")
    if system_rclone:
        return Path(system_rclone)

    # Check bundled binary
    ext = ".exe" if sys.platform == "win32" else ""
    bundled = _get_rclone_dir() / f"rclone{ext}"
    if bundled.exists():
        return bundled

    return None


def _get_platform_info() -> tuple[str, str, str]:
    """Get OS, arch, and file extension for rclone download.

    Returns:
        Tuple of (os_name, arch_name, extension).
    """
    system = platform.system().lower()
    machine = platform.machine().lower()

    if system == "windows":
        os_name = "windows"
        ext = ".exe"
    elif system == "darwin":
        os_name = "osx"
        ext = ""
    else:
        os_name = "linux"
        ext = ""

    if machine in ("x86_64", "amd64"):
        arch = "amd64"
    elif machine in ("aarch64", "arm64"):
        arch = "arm64"
    elif machine in ("i386", "i686"):
        arch = "386"
    else:
        arch = "amd64"  # Fallback

    return os_name, arch, ext


def _extract_zip_sync(zip_path: Path, target_path: Path, binary_name: str) -> bool:
    """Synchronous helper to extract rclone binary from zip."""
    with zipfile.ZipFile(zip_path, "r") as zf:
        for info in zf.infolist():
            if info.filename.endswith(binary_name) and not info.is_dir():
                with zf.open(info) as src:
                    target_path.write_bytes(src.read())
                return True
    return False


async def _download_rclone() -> Path | None:
    """Download rclone binary for current platform.

    Returns path to binary on success, None on failure.
    """
    os_name, arch, ext = _get_platform_info()
    archive_name = f"rclone-{settings.rclone_version}-{os_name}-{arch}.zip"
    url = f"https://github.com/rclone/rclone/releases/download/{settings.rclone_version}/{archive_name}"

    install_dir = _get_rclone_dir()
    install_dir.mkdir(parents=True, exist_ok=True)
    target_path = install_dir / f"rclone{ext}"

    if target_path.exists():
        return target_path

    logger.info(f"Downloading rclone {settings.rclone_version} for {os_name}-{arch}...")

    try:
        async with httpx.AsyncClient(follow_redirects=True) as client:
            response = await client.get(url, timeout=120.0)
            response.raise_for_status()

            # Write to temp file
            with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
                tmp.write(response.content)
                tmp_path = Path(tmp.name)

        # Verify SHA256 checksum
        expected_hash = _RCLONE_CHECKSUMS.get(f"{os_name}-{arch}")
        if expected_hash:
            sha256 = hashlib.sha256()
            with open(tmp_path, "rb") as f:
                while chunk := f.read(8192):
                    sha256.update(chunk)
            file_hash = sha256.hexdigest()

            if file_hash != expected_hash:
                tmp_path.unlink(missing_ok=True)
                logger.error(
                    f"Checksum mismatch for rclone download!\n"
                    f"Expected: {expected_hash}\n"
                    f"Got:      {file_hash}"
                )
                raise ValueError("SHA256 checksum verification failed")
        else:
            logger.warning(
                f"No checksum found for platform {os_name}-{arch}. "
                "Skipping verification."
            )

        # Extract rclone binary from zip
        binary_name = f"rclone{ext}"
        found = await asyncio.to_thread(
            _extract_zip_sync, tmp_path, target_path, binary_name
        )
        if not found:
            logger.error("rclone binary not found in archive")
            return None

        # Make executable on Unix
        if ext == "":
            target_path.chmod(target_path.stat().st_mode | stat.S_IEXEC)

        # Cleanup temp zip
        tmp_path.unlink(missing_ok=True)

        logger.info(f"rclone installed: {target_path}")
        return target_path

    except Exception as e:
        logger.error(f"Failed to download rclone: {e}")
        return None


async def ensure_rclone() -> Path | None:
    """Ensure rclone is available, downloading if needed.

    Returns path to rclone binary, or None if unavailable.
    """
    path = await asyncio.to_thread(_get_rclone_path)
    if path:
        return path

    # Download
    return await _download_rclone()


def _prepare_rclone_env() -> dict[str, str]:
    """Prepare env dict for rclone with auto-token management.

    Automatically sets ``RCLONE_CONFIG_<REMOTE>_TYPE`` from ``sync_provider``
    so users never need to set ``RCLONE_CONFIG_*`` env vars manually.

    Token resolution priority:
    1. Env var ``RCLONE_CONFIG_*_TOKEN`` (backward compatible)
    2. Local token file (~/.mnemo-mcp/tokens/<provider>.json)
    """
    from mnemo_mcp.token_store import load_token

    env = os.environ.copy()
    remote = settings.sync_remote
    remote_upper = remote.upper()
    token_key = f"RCLONE_CONFIG_{remote_upper}_TOKEN"
    type_key = f"RCLONE_CONFIG_{remote_upper}_TYPE"

    # Always set TYPE from sync_provider (user never needs RCLONE_CONFIG_*_TYPE)
    if type_key not in env:
        env[type_key] = settings.sync_provider

    # Priority 1: Env var tokens (decode base64 if needed)
    for key in list(env):
        if key.startswith("RCLONE_CONFIG_") and key.endswith("_TOKEN"):
            value = env[key]
            if value and not value.lstrip().startswith("{"):
                try:
                    decoded = base64.b64decode(value).decode("utf-8")
                    json.loads(decoded)  # Validate JSON structure
                    env[key] = decoded
                except Exception:
                    pass

    # Priority 2: Local token file (only if env var not set)
    if token_key not in env or not env[token_key]:
        token = load_token(settings.sync_provider)
        if token:
            env[token_key] = json.dumps(token)
            logger.debug(f"Using local token for {settings.sync_provider}")

    return env


def _run_rclone(
    rclone_path: Path, args: list[str], timeout: int = 120
) -> subprocess.CompletedProcess:
    """Run rclone command synchronously."""
    cmd = [str(rclone_path), *args]
    logger.debug(f"rclone: {' '.join(cmd)}")

    return subprocess.run(
        cmd,
        stdin=subprocess.DEVNULL,
        capture_output=True,
        text=True,
        timeout=timeout,
        env=_prepare_rclone_env(),
    )


async def check_remote_configured(rclone_path: Path, remote: str) -> bool:
    """Check if an rclone remote is configured."""
    result = await asyncio.to_thread(_run_rclone, rclone_path, ["listremotes"], 10)
    if result.returncode != 0:
        return False

    remotes = [
        r.strip().rstrip(":") for r in result.stdout.strip().split("\n") if r.strip()
    ]
    return remote in remotes


async def sync_push(rclone_path: Path, db_path: Path, remote: str, folder: str) -> bool:
    """Push local database to remote.

    Copies the SQLite database file to the remote folder.
    Uses rclone copy (not sync) to avoid deleting remote files.
    """
    remote_dest = f"{remote}:{folder}"

    logger.info(f"Pushing {db_path.name} to {remote_dest}...")

    result = await asyncio.to_thread(
        _run_rclone,
        rclone_path,
        ["copy", "--progress", "--", str(db_path), remote_dest],
        300,
    )

    if result.returncode == 0:
        logger.info(f"Push complete: {db_path.name} → {remote_dest}")
        return True
    else:
        logger.error(f"Push failed: {result.stderr[:300]}")
        return False


async def sync_pull(
    rclone_path: Path, db_path: Path, remote: str, folder: str
) -> Path | None:
    """Pull remote database to local temp directory.

    Downloads the remote DB file to a temp location for merging.
    Returns path to downloaded file, or None on failure.
    """
    remote_src = f"{remote}:{folder}/{db_path.name}"
    temp_dir = db_path.parent / "sync_temp"
    temp_dir.mkdir(parents=True, exist_ok=True)
    temp_db = temp_dir / f"remote_{db_path.name}"

    logger.info(f"Pulling from {remote_src}...")

    result = await asyncio.to_thread(
        _run_rclone,
        rclone_path,
        ["copyto", "--progress", "--", remote_src, str(temp_db)],
        300,
    )

    if result.returncode == 0 and temp_db.exists():
        logger.info(f"Pull complete: {remote_src} → {temp_db}")
        return temp_db
    else:
        logger.warning(f"Pull failed or no remote file: {result.stderr[:200]}")
        # Cleanup
        temp_db.unlink(missing_ok=True)
        return None


async def sync_full(db: MemoryDB) -> dict:
    """Full sync cycle: pull → merge → push.

    Auto-provisions tokens if needed (interactive browser auth on first run).

    Returns:
        Dict with sync results.
    """
    from mnemo_mcp.db import MemoryDB

    if not settings.sync_enabled:
        return {"status": "disabled", "message": "Sync not configured"}

    rclone_path = await ensure_rclone()
    if not rclone_path:
        return {"status": "error", "message": "rclone not available"}

    # Auto-provision token if needed
    if not _has_token_available():
        token = await _interactive_auth(rclone_path, settings.sync_provider)
        if not token:
            return {
                "status": "error",
                "message": "No sync token available. "
                "Run the server interactively to complete OAuth setup.",
            }

    # Check remote is configured (env vars or local token loaded by _prepare_rclone_env)
    if not await check_remote_configured(rclone_path, settings.sync_remote):
        return {
            "status": "error",
            "message": f"rclone remote '{settings.sync_remote}' not configured. "
            "Token may be invalid — try deleting the token file and re-authenticating.",
        }

    db_path = settings.get_db_path()
    remote = settings.sync_remote
    folder = settings.sync_folder

    result: dict = {"status": "ok", "pull": None, "push": None}

    # 1. Pull remote DB
    remote_db_path = await sync_pull(rclone_path, db_path, remote, folder)
    if remote_db_path:
        try:

            def _merge_dbs() -> dict:
                _remote_db = MemoryDB(remote_db_path, embedding_dims=0)
                _remote_jsonl, _ = _remote_db.export_jsonl()
                _remote_db.close()
                if _remote_jsonl.strip():
                    return db.import_jsonl(_remote_jsonl, mode="merge")
                return {"imported": 0, "skipped": 0}

            # Run DB operations in thread pool to prevent blocking asyncio loop
            import_result = await asyncio.to_thread(_merge_dbs)

            result["pull"] = import_result
            if import_result.get("imported", 0) > 0:
                logger.info(f"Merged {import_result['imported']} memories from remote")

        except Exception as e:
            logger.error(f"Merge failed: {e}")
            result["pull"] = {"error": str(e)}
        finally:
            # Cleanup temp file and directory
            remote_db_path.unlink(missing_ok=True)
            try:
                remote_db_path.parent.rmdir()
            except OSError:
                pass
    else:
        result["pull"] = {"imported": 0, "skipped": 0, "note": "No remote DB found"}

    # 2. Push local DB to remote
    push_ok = await sync_push(rclone_path, db_path, remote, folder)
    result["push"] = {"success": push_ok}

    return result


async def _auto_sync_loop(db: MemoryDB) -> None:
    """Background auto-sync loop."""
    interval = settings.sync_interval
    if interval <= 0:
        return

    logger.info(f"Auto-sync started (interval={interval}s)")
    # Run first sync immediately (don't wait for interval)
    try:
        await sync_full(db)
    except asyncio.CancelledError:
        logger.info("Auto-sync stopped")
        return
    except Exception as e:
        logger.error(f"Initial sync error: {e}")

    while True:
        try:
            await asyncio.sleep(interval)
            await sync_full(db)
        except asyncio.CancelledError:
            logger.info("Auto-sync stopped")
            break
        except Exception as e:
            logger.error(f"Auto-sync error: {e}")
            # Continue running despite errors


def start_auto_sync(db: MemoryDB) -> None:
    """Start background auto-sync task."""
    global _sync_task

    if not settings.sync_enabled or settings.sync_interval <= 0:
        return

    if _sync_task and not _sync_task.done():
        return  # Already running

    _sync_task = asyncio.create_task(_auto_sync_loop(db))


def stop_auto_sync() -> None:
    """Stop background auto-sync task."""
    global _sync_task
    if _sync_task and not _sync_task.done():
        _sync_task.cancel()
        _sync_task = None


def _has_token_available() -> bool:
    """Check if a sync token is available (env var or local file)."""
    from mnemo_mcp.token_store import load_token

    remote_upper = settings.sync_remote.upper()
    token_key = f"RCLONE_CONFIG_{remote_upper}_TOKEN"

    # Check env var
    if os.environ.get(token_key):
        return True

    # Check local token file
    return load_token(settings.sync_provider) is not None


async def _interactive_auth(rclone_path: Path, provider: str) -> dict | None:
    """Run rclone authorize interactively to get OAuth token.

    Opens browser for user authentication. Blocks until complete
    or timeout (5 minutes).

    Returns token dict on success, None on failure.
    """
    from mnemo_mcp.config import RCLONE_PROVIDERS
    from mnemo_mcp.token_store import save_token

    if provider not in RCLONE_PROVIDERS:
        logger.error(f"Invalid provider: {provider}")
        return None

    logger.info(f"No sync token found. Starting {provider} authentication...")
    logger.info("A browser window will open for authentication.")

    result = await asyncio.to_thread(
        lambda: subprocess.run(
            [str(rclone_path), "authorize", "--", provider],
            stdout=subprocess.PIPE,
            text=True,
            timeout=300,
        )
    )

    if result.returncode != 0:
        logger.error(f"Authentication failed (exit {result.returncode})")
        return None

    token_str = _extract_token(result.stdout or "")
    if not token_str:
        logger.error("Could not extract token from rclone output")
        return None

    try:
        token = json.loads(token_str)
    except json.JSONDecodeError:
        logger.error("Invalid token JSON from rclone")
        return None

    save_token(provider, token)
    logger.info("Authentication successful! Token saved locally.")
    return token


def setup_sync(remote_type: str = "drive") -> None:
    """Download rclone, run authorize, and save token locally.

    Usage: mnemo-mcp setup-sync [type]
    Default type: drive (Google Drive)

    Token is saved to ~/.mnemo-mcp/tokens/<type>.json so no env vars
    are needed for sync — just set SYNC_ENABLED=true.
    """
    from mnemo_mcp.config import RCLONE_PROVIDERS
    from mnemo_mcp.token_store import save_token

    if remote_type not in RCLONE_PROVIDERS:
        print(f"ERROR: Invalid provider type '{remote_type}'.", file=sys.stderr)
        print(f"Must be one of: {', '.join(sorted(RCLONE_PROVIDERS))}", file=sys.stderr)
        sys.exit(1)

    print(f"=== Mnemo MCP: Setup Sync ({remote_type}) ===\n")

    # 1. Ensure rclone is available
    rclone_path = _get_rclone_path()
    if rclone_path:
        print(f"rclone found: {rclone_path}")
    else:
        print("Downloading rclone...")
        rclone_path = asyncio.run(_download_rclone())
        if not rclone_path:
            print("ERROR: Failed to download rclone", file=sys.stderr)
            sys.exit(1)
        print(f"rclone installed: {rclone_path}")

    # 2. Run rclone authorize (interactive — opens browser)
    print(f'\nRunning: rclone authorize "{remote_type}"')
    print("A browser window will open for authentication.\n")
    print("-" * 50)

    # Capture stdout (contains token JSON), let stderr pass through
    # so user sees rclone progress messages in real-time
    result = subprocess.run(
        [str(rclone_path), "authorize", "--", remote_type],
        stdout=subprocess.PIPE,
        text=True,
        timeout=300,
    )

    print("-" * 50)

    if result.returncode != 0:
        print(
            f"\nERROR: rclone authorize failed (exit {result.returncode})",
            file=sys.stderr,
        )
        sys.exit(1)

    # 3. Extract token JSON from stdout
    token_json = _extract_token(result.stdout or "")

    remote_name = "gdrive" if remote_type == "drive" else remote_type

    if token_json:
        # Save token locally
        try:
            token_dict = json.loads(token_json)
            save_token(remote_type, token_dict)
            from mnemo_mcp.token_store import get_token_path

            token_path = get_token_path(remote_type)

            print(f"\n{'=' * 60}")
            print("SUCCESS! Token saved locally.")
            print(f"{'=' * 60}\n")
            print(f"Token file: {token_path}")
            print("\nAll you need in your MCP config:")
            print('  "SYNC_ENABLED": "true"')
            if remote_type != "drive":
                print(f'  "SYNC_PROVIDER": "{remote_type}"')
            if remote_name != "gdrive":
                print(f'  "SYNC_REMOTE": "{remote_name}"')
            print("\nThe server will auto-load the token from disk.")
            print("No need to copy/paste any tokens!")
        except json.JSONDecodeError:
            # Fallback: print base64 for manual config
            token_b64 = base64.b64encode(token_json.encode()).decode()
            print(f"\n{'=' * 60}")
            print("Token saved but could not parse JSON. Base64 token:")
            print(f"{'=' * 60}\n")
            print(token_b64)
    else:
        print(f"\n{'=' * 60}")
        print("MANUAL SETUP")
        print(f"{'=' * 60}\n")
        print("Could not auto-extract token from rclone output.")
        print("Try running the server with SYNC_ENABLED=true — it will")
        print("open a browser for authentication automatically.")


def _extract_token(output: str) -> str | None:
    """Extract rclone OAuth token JSON from authorize output.

    rclone outputs the token between dashed lines like:
        Paste the following into your remote machine config
        --------------------
        {"access_token":"...","token_type":"Bearer",...}
        --------------------
    """
    import re

    # Try to find JSON between ---- markers
    pattern = r"-{4,}\s*\n\s*(\{[^}]+\})\s*\n\s*-{4,}"
    match = re.search(pattern, output)
    if match:
        return match.group(1).strip()

    # Fallback: find any JSON object with access_token
    pattern = r'\{"access_token"[^}]+\}'
    match = re.search(pattern, output)
    if match:
        return match.group(0).strip()

    return None
