"""Configuration synchronization for OpenShift pods."""

from __future__ import annotations

import os
import sys
import tempfile
from pathlib import Path

from paude.agents import get_agent
from paude.backends.openshift.exceptions import OcTimeoutError, OpenShiftError
from paude.backends.openshift.oc import (
    OC_EXEC_TIMEOUT,
    RSYNC_MAX_RETRIES,
    RSYNC_TIMEOUT,
    OcClient,
)
from paude.backends.shared import config_file_basename
from paude.constants import CONTAINER_HOME, GCP_ADC_FILENAME


class ConfigSyncer:
    """Handles configuration and credential synchronization to OpenShift pods.

    This class is responsible for syncing local configuration files
    (gcloud credentials, claude config, gitconfig) to remote pods.
    """

    def __init__(self, oc: OcClient, namespace: str) -> None:
        """Initialize the ConfigSyncer.

        Args:
            oc: OcClient instance for running oc commands.
            namespace: Kubernetes namespace for operations.
        """
        self._oc = oc
        self._namespace = namespace

    def rsync_with_retry(
        self,
        source: str,
        dest: str,
        exclude_args: list[str],
        verbose: bool = False,
        delete: bool = False,
    ) -> bool:
        """Run oc rsync with retry logic for timeouts.

        Args:
            source: Source path (local or pod:path format).
            dest: Destination path (local or pod:path format).
            exclude_args: List of --exclude arguments.
            verbose: Whether to show rsync output (default False).
            delete: Whether to delete files not in source (default False).

        Returns:
            True if sync succeeded, False if all retries failed.
        """
        for attempt in range(1, RSYNC_MAX_RETRIES + 1):
            try:
                rsync_args = [
                    "rsync",
                    "--progress",
                    source,
                    dest,
                    "--no-perms",
                ]
                if delete:
                    rsync_args.append("--delete")
                rsync_args.extend(exclude_args)

                result = self._oc.run(
                    *rsync_args,
                    timeout=RSYNC_TIMEOUT,
                    capture=True,
                    check=False,
                    namespace=self._namespace,
                )

                if verbose and result.stdout:
                    print(result.stdout, file=sys.stderr)

                if result.returncode != 0:
                    print(
                        f"Rsync failed: {result.stderr.strip() or 'unknown error'}",
                        file=sys.stderr,
                    )
                    return False

                return True
            except OcTimeoutError:
                if attempt < RSYNC_MAX_RETRIES:
                    print(
                        f"Rsync timed out (attempt {attempt}/{RSYNC_MAX_RETRIES}), "
                        "retrying...",
                        file=sys.stderr,
                    )
                else:
                    print(
                        f"Rsync failed after {RSYNC_MAX_RETRIES} attempts",
                        file=sys.stderr,
                    )
                    return False
        return False

    def is_config_synced(self, pod_name: str) -> bool:
        """Check if configuration has already been synced to the pod.

        Returns True if /credentials/.ready exists, indicating a previous
        full config sync.
        """
        config_path = "/credentials"
        result = self._oc.run(
            "exec",
            pod_name,
            "-n",
            self._namespace,
            "--",
            "test",
            "-f",
            f"{config_path}/.ready",
            check=False,
            timeout=OC_EXEC_TIMEOUT,
        )
        return result.returncode == 0

    def _cp_content_to_pod(self, pod_name: str, content: str, dest_path: str) -> None:
        """Write content to a tempfile and copy it to the pod via ``oc cp``."""
        try:
            with tempfile.NamedTemporaryFile(mode="w", suffix=".secret") as tmp:
                tmp.write(content)
                tmp.flush()
                self._oc.run(
                    "cp",
                    tmp.name,
                    f"{pod_name}:{dest_path}",
                    "-n",
                    self._namespace,
                    check=False,
                )
        except Exception:  # noqa: S110
            pass

    def _sync_secret_env_vars(self, pod_name: str, secret_env: dict[str, str]) -> None:
        """Sync secret environment variables to /credentials/env/ on the pod.

        Each variable is written to a tempfile and copied via ``oc cp`` to
        ``/credentials/env/<VAR_NAME>``. The entrypoint reads these files and
        exports them, keeping secrets out of the StatefulSet spec.
        """
        if not secret_env:
            return

        config_path = "/credentials"
        self._oc.run(
            "exec",
            pod_name,
            "-n",
            self._namespace,
            "--",
            "mkdir",
            "-p",
            f"{config_path}/env",
            check=False,
            timeout=OC_EXEC_TIMEOUT,
        )

        for var_name, value in secret_env.items():
            self._cp_content_to_pod(pod_name, value, f"{config_path}/env/{var_name}")

    def _sync_github_token(self, pod_name: str, github_token: str | None) -> None:
        """Sync GitHub token to the pod's credentials directory.

        Args:
            pod_name: Name of the pod to sync to.
            github_token: Token value, or None to read from PAUDE_GITHUB_TOKEN env.
        """
        token = github_token or os.environ.get("PAUDE_GITHUB_TOKEN")
        if not token:
            return

        self._cp_content_to_pod(pod_name, token, "/credentials/github_token")

    def _sync_cursor_auth_json(self, pod_name: str) -> None:
        """Sync Cursor auth.json from ~/.config/cursor/ to /credentials/."""
        auth_json = Path.home() / ".config" / "cursor" / "auth.json"
        if auth_json.is_file():
            try:
                self._oc.run(
                    "cp",
                    str(auth_json),
                    f"{pod_name}:/credentials/cursor-auth.json",
                    "-n",
                    self._namespace,
                    check=False,
                )
            except Exception:  # noqa: S110
                pass

    def sync_credentials(
        self,
        pod_name: str,
        verbose: bool = False,
        github_token: str | None = None,
        secret_env: dict[str, str] | None = None,
        agent_name: str = "claude",
    ) -> None:
        """Refresh gcloud credentials on the pod (fast, every connect).

        Only syncs gcloud credential files. Used on reconnects when full
        config is already present. Also syncs GitHub token and secret env
        vars if available.

        Args:
            pod_name: Name of the pod to sync to.
            verbose: Whether to show sync progress.
            github_token: Optional GitHub token to inject into pod tmpfs.
                Falls back to PAUDE_GITHUB_TOKEN env var if not provided.
            secret_env: Secret environment variables to sync to tmpfs.
            agent_name: Agent name (used for agent-specific credential sync).
        """
        home = Path.home()
        config_path = "/credentials"

        print("Refreshing credentials...", file=sys.stderr)

        gcloud_dir = home / ".config" / "gcloud"
        gcloud_files = [
            GCP_ADC_FILENAME,
            "credentials.db",
            "access_tokens.db",
        ]
        gcloud_dest = f"{pod_name}:{config_path}/gcloud"
        for filename in gcloud_files:
            filepath = gcloud_dir / filename
            if filepath.exists():
                try:
                    self._oc.run(
                        "cp",
                        str(filepath),
                        f"{gcloud_dest}/{filename}",
                        "-n",
                        self._namespace,
                        check=False,
                    )
                except Exception:  # noqa: S110
                    pass

        self._sync_github_token(pod_name, github_token)
        self._sync_secret_env_vars(pod_name, secret_env or {})

        if agent_name == "cursor":
            self._sync_cursor_auth_json(pod_name)

        self._oc.run(
            "exec",
            pod_name,
            "-n",
            self._namespace,
            "--",
            "touch",
            f"{config_path}/.ready",
            check=False,
            timeout=OC_EXEC_TIMEOUT,
        )

        if verbose:
            print("  Refreshed gcloud credentials", file=sys.stderr)
        print("Credentials refreshed.", file=sys.stderr)

    def _prepare_config_directory(
        self, pod_name: str, agent_name: str = "claude"
    ) -> None:
        """Prepare the config directory on the pod."""
        config_path = "/credentials"
        prep_result = self._oc.run(
            "exec",
            pod_name,
            "-n",
            self._namespace,
            "--",
            "bash",
            "-c",
            f"mkdir -p {config_path}/gcloud {config_path}/{agent_name} && "
            f"(chmod -R g+rwX {config_path} 2>/dev/null || true)",
            check=False,
            timeout=OC_EXEC_TIMEOUT,
        )
        if prep_result.returncode != 0:
            raise OpenShiftError(
                f"Failed to prepare config directory: {prep_result.stderr}"
            )

    def _sync_gcloud_credentials(self, pod_name: str) -> None:
        """Sync gcloud credentials to the pod."""
        home = Path.home()
        config_path = "/credentials"
        gcloud_dir = home / ".config" / "gcloud"
        gcloud_files = [
            GCP_ADC_FILENAME,
            "credentials.db",
            "access_tokens.db",
        ]
        gcloud_dest = f"{pod_name}:{config_path}/gcloud"
        for filename in gcloud_files:
            filepath = gcloud_dir / filename
            if filepath.exists():
                try:
                    self._oc.run(
                        "cp",
                        str(filepath),
                        f"{gcloud_dest}/{filename}",
                        "-n",
                        self._namespace,
                        check=False,
                    )
                except Exception:  # noqa: S110
                    pass

    def _sync_agent_config(
        self,
        pod_name: str,
        verbose: bool = False,
        agent_name: str = "claude",
    ) -> bool:
        """Sync agent config directory to the pod.

        Returns True if sync succeeded.
        """
        home = Path.home()
        config_path = "/credentials"
        agent = get_agent(agent_name)
        config_dir = home / agent.config.config_dir_name
        config_file = (
            home / agent.config.config_file_name
            if agent.config.config_file_name
            else None
        )

        if config_dir.is_dir():
            if agent.config.config_sync_files_only:
                # Only copy specific files instead of rsyncing the entire directory
                for filename in agent.config.config_sync_files_only:
                    filepath = config_dir / filename
                    try:
                        self._oc.run(
                            "cp",
                            str(filepath),
                            f"{pod_name}:{config_path}/{agent_name}/{filename}",
                            "-n",
                            self._namespace,
                            check=False,
                        )
                    except Exception:  # noqa: S110
                        pass
                if verbose:
                    cfg_dir = agent.config.config_dir_name
                    files = ", ".join(agent.config.config_sync_files_only)
                    print(
                        f"  Synced ~/{cfg_dir}/ ({files} only)",
                        file=sys.stderr,
                    )
            else:
                exclude_args = []
                for pattern in agent.config.config_excludes:
                    exclude_args.extend(["--exclude", pattern])

                rsync_success = self.rsync_with_retry(
                    f"{config_dir}/",
                    f"{pod_name}:{config_path}/{agent_name}",
                    exclude_args,
                    verbose=verbose,
                )

                if rsync_success:
                    self._rewrite_plugin_paths(
                        pod_name,
                        config_path,
                        agent_name=agent_name,
                        config_dir_name=agent.config.config_dir_name,
                    )
                    if verbose:
                        cfg_dir = agent.config.config_dir_name
                        print(
                            f"  Synced ~/{cfg_dir}/ (including plugins)",
                            file=sys.stderr,
                        )
                else:
                    cfg_dir = agent.config.config_dir_name
                    print(
                        f"  Warning: Failed to sync ~/{cfg_dir}/"
                        " - plugins may not work",
                        file=sys.stderr,
                    )
                    return False

        # Sync Cursor auth.json from ~/.config/cursor/ (separate from ~/.cursor/)
        if agent_name == "cursor":
            self._sync_cursor_auth_json(pod_name)

        if config_file and config_file.exists():
            try:
                basename = (
                    config_file_basename(agent.config.config_file_name)
                    if agent.config.config_file_name
                    else ""
                )
                dest = f"{pod_name}:{config_path}/{agent_name}/{basename}"
                self._oc.run(
                    "cp",
                    str(config_file),
                    dest,
                    "-n",
                    self._namespace,
                    check=False,
                )
            except Exception:  # noqa: S110
                pass

        return True

    def _sync_gitconfig(self, pod_name: str, verbose: bool = False) -> None:
        """Sync gitconfig to the pod."""
        home = Path.home()
        config_path = "/credentials"
        gitconfig = home / ".gitconfig"
        if gitconfig.exists():
            try:
                self._oc.run(
                    "cp",
                    str(gitconfig),
                    f"{pod_name}:{config_path}/gitconfig",
                    "-n",
                    self._namespace,
                    check=False,
                )
                if verbose:
                    print("  Synced ~/.gitconfig", file=sys.stderr)
            except Exception:  # noqa: S110
                pass

    def _sync_global_gitignore(self, pod_name: str, verbose: bool = False) -> None:
        """Sync global gitignore to the pod."""
        home = Path.home()
        config_path = "/credentials"
        global_gitignore = home / ".config" / "git" / "ignore"
        if global_gitignore.exists():
            try:
                self._oc.run(
                    "cp",
                    str(global_gitignore),
                    f"{pod_name}:{config_path}/gitignore-global",
                    "-n",
                    self._namespace,
                    check=False,
                )
                if verbose:
                    print(
                        "  Synced ~/.config/git/ignore (global gitignore)",
                        file=sys.stderr,
                    )
            except Exception:  # noqa: S110
                pass

    def _finalize_sync(self, pod_name: str) -> None:
        """Finalize sync by setting permissions and creating .ready marker."""
        config_path = "/credentials"
        self._oc.run(
            "exec",
            pod_name,
            "-n",
            self._namespace,
            "--",
            "bash",
            "-c",
            f"(chmod -R g+rX {config_path} 2>/dev/null || true) && "
            f"touch {config_path}/.ready && "
            f"chmod g+r {config_path}/.ready",
            check=False,
            timeout=OC_EXEC_TIMEOUT,
        )

        verify_result = self._oc.run(
            "exec",
            pod_name,
            "-n",
            self._namespace,
            "--",
            "test",
            "-f",
            f"{config_path}/.ready",
            check=False,
            timeout=OC_EXEC_TIMEOUT,
        )
        if verify_result.returncode != 0:
            print(
                f"Warning: Failed to create {config_path}/.ready marker",
                file=sys.stderr,
            )

    def sync_full_config(
        self,
        pod_name: str,
        verbose: bool = False,
        github_token: str | None = None,
        agent_name: str = "claude",
        secret_env: dict[str, str] | None = None,
    ) -> None:
        """Sync all configuration to pod /credentials/ directory.

        Full sync including gcloud credentials, agent config, gitconfig,
        global gitignore, optional GitHub token, and secret env vars.

        Args:
            pod_name: Name of the pod to sync to.
            verbose: Whether to show sync progress.
            github_token: Optional GitHub token to inject into pod tmpfs.
                Falls back to PAUDE_GITHUB_TOKEN env var if not provided.
            agent_name: Agent name for config directory naming.
            secret_env: Secret environment variables to sync to tmpfs.
        """
        print("Syncing configuration to pod...", file=sys.stderr)

        self._prepare_config_directory(pod_name, agent_name=agent_name)
        self._sync_gcloud_credentials(pod_name)
        self._sync_agent_config(pod_name, verbose=verbose, agent_name=agent_name)
        self._sync_gitconfig(pod_name, verbose=verbose)
        self._sync_global_gitignore(pod_name, verbose=verbose)
        self._sync_github_token(pod_name, github_token)
        self._sync_secret_env_vars(pod_name, secret_env or {})
        self._finalize_sync(pod_name)

        print("Configuration synced.", file=sys.stderr)

    def _rewrite_plugin_paths(
        self,
        pod_name: str,
        config_path: str,
        agent_name: str = "claude",
        config_dir_name: str = ".claude",
    ) -> None:
        """Rewrite absolute paths in plugin metadata files using jq.

        Agent tools write plugin paths as absolute host paths. These need
        to be rewritten to container paths.
        """
        container_plugins_path = f"{CONTAINER_HOME}/{config_dir_name}/plugins"

        installed_plugins = f"{config_path}/{agent_name}/plugins/installed_plugins.json"
        jq_expr = (
            ".plugins |= with_entries(.value |= map("
            "if .installPath then "
            '.installPath = ($prefix + "/" + '
            '(.installPath | split("/") | .[-3:] | join("/"))) '
            "else . end))"
        )
        self._oc.run(
            "exec",
            pod_name,
            "-n",
            self._namespace,
            "--",
            "bash",
            "-c",
            f'if [ -f "{installed_plugins}" ]; then '
            f"jq --arg prefix \"{container_plugins_path}/cache\" '{jq_expr}' "
            f'"{installed_plugins}" > "{installed_plugins}.tmp" && '
            f'mv "{installed_plugins}.tmp" "{installed_plugins}"; fi',
            check=False,
            timeout=OC_EXEC_TIMEOUT,
        )

        known_marketplaces = (
            f"{config_path}/{agent_name}/plugins/known_marketplaces.json"
        )
        jq_expr2 = (
            "with_entries(if .value.installLocation then "
            '.value.installLocation = ($prefix + "/marketplaces/" + .key) '
            "else . end)"
        )
        self._oc.run(
            "exec",
            pod_name,
            "-n",
            self._namespace,
            "--",
            "bash",
            "-c",
            f'if [ -f "{known_marketplaces}" ]; then '
            f"jq --arg prefix \"{container_plugins_path}\" '{jq_expr2}' "
            f'"{known_marketplaces}" > "{known_marketplaces}.tmp" && '
            f'mv "{known_marketplaces}.tmp" "{known_marketplaces}"; fi',
            check=False,
            timeout=OC_EXEC_TIMEOUT,
        )
