import json
import os
import sys
import tempfile
import subprocess
import base64
from pathlib import Path
from typing import Dict, List, Optional, Tuple

from .format import LockboxFormat
from .gpg_ops import GPGOperations

# Optional agent client for caching decrypted lockbox data (best-effort)
try:
    from . import agent_client
except Exception:
    agent_client = None


class Lockbox:
    """Manage GPG-encrypted lockbox with secrets."""

    def __init__(self, path: str, quiet: bool = False, use_agent: bool = True):
        self.path = Path(path)
        self.quiet = quiet
        self.use_agent = use_agent
        self.gpg = GPGOperations(quiet=quiet)
        self._ensure_extension()

    def _ensure_extension(self):
        """Ensure lockbox file has .gpb extension."""
        if self.path.suffix != ".gpb":
            raise ValueError("[GPMASTER:] Lockbox file must have .gpb extension")

    def _load_format(self) -> LockboxFormat:
        """Load and validate lockbox format."""
        if not self.path.exists():
            raise FileNotFoundError(f"[GPMASTER:] Lockbox not found: {self.path}")

        with open(self.path, "rb") as f:
            data = f.read()

        return LockboxFormat.unpack(data)

    def _decrypt_data(
        self, fmt: LockboxFormat, retry: bool = True
    ) -> Tuple[bool, Optional[Dict], Optional[str]]:
        """Decrypt lockbox data, with optional agent cache support."""
        # Try agent cache first (best-effort)
        if getattr(self, "use_agent", True) and agent_client is not None:
            try:
                cached = agent_client.get_cached(str(self.path))
                if cached is not None:
                    try:
                        secrets = json.loads(cached.decode("utf-8"))
                        return True, secrets, fmt.key_id
                    except Exception:
                        # fall through to normal decrypt
                        pass
            except Exception:
                # agent not available; fall back to gpg
                pass

        success, decrypted, key_id = self.gpg.decrypt(fmt.encrypted_data, retry=retry)
        if not success:
            return False, None, None

        try:
            secrets = json.loads(decrypted.decode("utf-8"))
            # Store in agent cache for future use (best-effort)
            if getattr(self, "use_agent", True) and agent_client is not None:
                try:
                    agent_client.set_cached(str(self.path), decrypted)
                except Exception:
                    pass

            return True, secrets, key_id
        except (json.JSONDecodeError, UnicodeDecodeError):
            return False, None, None

    def _save_lockbox(self, fmt: LockboxFormat):
        """Save lockbox to disk."""
        self.path.parent.mkdir(parents=True, exist_ok=True)

        data = fmt.pack()
        with open(self.path, "wb") as f:
            f.write(data)

    def create(self, key_id: str):
        """Create a new lockbox."""
        if self.path.exists():
            raise FileExistsError(f"[GPMASTER:] Lockbox already exists: {self.path}")

        secrets = {}
        secrets_json = json.dumps(secrets, separators=(",", ":")).encode("utf-8")

        success, encrypted = self.gpg.encrypt(secrets_json, key_id, retry=False)
        if not success:
            raise RuntimeError("[GPMASTER:] Failed to encrypt lockbox")

        fmt = LockboxFormat()
        fmt.key_id = key_id
        fmt.encrypted_data = encrypted
        fmt.titles = []
        fmt.note_content = None
        fmt.note_signature = None

        data_to_sign = fmt.key_id.encode("utf-8") + encrypted
        success, signature = self.gpg.sign(data_to_sign, key_id, retry=True)
        if success:
            fmt.signature = signature

        self._save_lockbox(fmt)

        # Update agent cache with new plaintext secrets (best-effort)
        if getattr(self, "use_agent", True) and agent_client is not None:
            try:
                agent_client.set_cached(str(self.path), secrets_json)
            except Exception:
                pass

        if not self.quiet:
            print(f"[GPMASTER:] Created lockbox encrypted with key: {key_id}")

    def validate(self) -> bool:
        """Validate lockbox integrity and signature."""
        try:
            fmt = self._load_format()

            if not self.quiet:
                print(f"[GPMASTER:] Lockbox encrypted with key: {fmt.key_id}")

            if fmt.signature:
                data_to_verify = fmt.key_id.encode("utf-8") + fmt.encrypted_data
                valid, signer_key = self.gpg.verify(data_to_verify, fmt.signature)

                if valid:
                    if not self.quiet:
                        print(f"[GPMASTER:] Signature valid (signed by: {signer_key})")
                    return True
                else:
                    if not self.quiet:
                        print("[GPMASTER:] Signature validation failed")
                    return False
            else:
                if not self.quiet:
                    print("[GPMASTER:] No signature present")
                return True

        except Exception as e:
            if not self.quiet:
                print(f"[GPMASTER:] Validation failed: {e}")
            return False

    def list_contents(self) -> Tuple[List[str], str]:
        """List lockbox contents without decryption (kept for compatibility)."""
        fmt = self._load_format()
        return fmt.titles, fmt.key_id

    def get_info(self) -> Tuple[List[str], Optional[str], Optional[bytes], str]:
        """Get lockbox info including note content and signature."""
        fmt = self._load_format()
        return fmt.titles, fmt.note_content, fmt.note_signature, fmt.key_id

    def add_secret(
        self,
        name: str,
        secret: str,
        is_totp: bool = False,
        auto_create_key: Optional[str] = None,
    ):
        """Add or update a secret in the lockbox."""
        if self.path.exists():
            fmt = self._load_format()
            success, secrets, dec_key = self._decrypt_data(fmt, retry=not self.quiet)
            if not success:
                raise RuntimeError("[GPMASTER:] Failed to decrypt lockbox")

            if not self.quiet:
                print(f"[GPMASTER:] Decrypted with key: {dec_key}")
        else:
            if not auto_create_key:
                raise FileNotFoundError(
                    "[GPMASTER:] Lockbox does not exist. Create it first with 'gpmaster create <key-id>' or set GPMASTER_KEY_ID environment variable."
                )

            if not self.quiet:
                print(f"[GPMASTER:] Creating new lockbox with key: {auto_create_key}")
            self.create(auto_create_key)

            fmt = self._load_format()
            secrets = {}

        entry = {"value": secret}
        if is_totp:
            entry["type"] = "totp"

        secrets[name] = entry

        if name not in fmt.titles:
            fmt.titles.append(name)

        secrets_json = json.dumps(secrets, separators=(",", ":")).encode("utf-8")
        success, encrypted = self.gpg.encrypt(
            secrets_json, fmt.key_id, retry=not self.quiet
        )
        if not success:
            raise RuntimeError("[GPMASTER:] Failed to encrypt lockbox")

        fmt.encrypted_data = encrypted

        data_to_sign = fmt.key_id.encode("utf-8") + encrypted
        success, signature = self.gpg.sign(data_to_sign, fmt.key_id, retry=True)
        if success:
            fmt.signature = signature

        self._save_lockbox(fmt)

        # Update agent cache with new plaintext secrets (best-effort)
        if getattr(self, "use_agent", True) and agent_client is not None:
            try:
                agent_client.set_cached(str(self.path), secrets_json)
            except Exception:
                pass

        if not self.quiet:
            print(f"[GPMASTER:] Added secret: {name}")

    def get_secret(self, name: str) -> Tuple[Optional[str], bool]:
        """Retrieve a secret from the lockbox."""
        fmt = self._load_format()

        if not self.quiet:
            print(f"[GPMASTER:] Encrypted with key: {fmt.key_id}")

        success, secrets, dec_key = self._decrypt_data(fmt, retry=not self.quiet)
        if not success:
            raise RuntimeError("[GPMASTER:] Failed to decrypt lockbox")

        if not self.quiet:
            print(f"[GPMASTER:] Decrypted with key: {dec_key}")

        if name not in secrets:
            return None, False

        entry = secrets[name]
        is_totp = entry.get("type") == "totp"
        return entry["value"], is_totp

    def rename_secret(self, old_name: str, new_name: str):
        """Rename a secret."""
        fmt = self._load_format()
        success, secrets, dec_key = self._decrypt_data(fmt, retry=not self.quiet)
        if not success:
            raise RuntimeError("[GPMASTER:] Failed to decrypt lockbox")

        if not self.quiet:
            print(f"[GPMASTER:] Decrypted with key: {dec_key}")

        if old_name not in secrets:
            raise KeyError(f"[GPMASTER:] Secret not found: {old_name}")

        if new_name in secrets:
            raise KeyError(f"[GPMASTER:] Secret already exists: {new_name}")

        secrets[new_name] = secrets.pop(old_name)

        if old_name in fmt.titles:
            idx = fmt.titles.index(old_name)
            fmt.titles[idx] = new_name

        secrets_json = json.dumps(secrets, separators=(",", ":")).encode("utf-8")
        success, encrypted = self.gpg.encrypt(
            secrets_json, fmt.key_id, retry=not self.quiet
        )
        if not success:
            raise RuntimeError("[GPMASTER:] Failed to encrypt lockbox")

        fmt.encrypted_data = encrypted

        data_to_sign = fmt.key_id.encode("utf-8") + encrypted
        success, signature = self.gpg.sign(data_to_sign, fmt.key_id, retry=True)
        if success:
            fmt.signature = signature

        self._save_lockbox(fmt)

        # Update agent cache with new plaintext secrets (best-effort)
        if getattr(self, "use_agent", True) and agent_client is not None:
            try:
                agent_client.set_cached(str(self.path), secrets_json)
            except Exception:
                pass

        if not self.quiet:
            print(f"[GPMASTER:] Renamed: {old_name} -> {new_name}")

    def delete_secret(self, name: str):
        """Delete a secret."""
        fmt = self._load_format()
        success, secrets, dec_key = self._decrypt_data(fmt, retry=not self.quiet)
        if not success:
            raise RuntimeError("[GPMASTER:] Failed to decrypt lockbox")

        if not self.quiet:
            print(f"[GPMASTER:] Decrypted with key: {dec_key}")

        if name not in secrets:
            raise KeyError(f"[GPMASTER:] Secret not found: {name}")

        del secrets[name]

        if name in fmt.titles:
            fmt.titles.remove(name)

        secrets_json = json.dumps(secrets, separators=(",", ":")).encode("utf-8")
        success, encrypted = self.gpg.encrypt(
            secrets_json, fmt.key_id, retry=not self.quiet
        )
        if not success:
            raise RuntimeError("[GPMASTER:] Failed to encrypt lockbox")

        fmt.encrypted_data = encrypted

        data_to_sign = fmt.key_id.encode("utf-8") + encrypted
        success, signature = self.gpg.sign(data_to_sign, fmt.key_id, retry=True)
        if success:
            fmt.signature = signature

        self._save_lockbox(fmt)

        # Update agent cache with new plaintext secrets (best-effort)
        if getattr(self, "use_agent", True) and agent_client is not None:
            try:
                agent_client.set_cached(str(self.path), secrets_json)
            except Exception:
                pass

        if not self.quiet:
            print(f"[GPMASTER:] Deleted secret: {name}")

    def edit_note(self):
        """Edit notes document with $EDITOR and sign it."""
        fmt = self._load_format()

        current_content = fmt.note_content if fmt.note_content else ""

        editor = os.environ.get("EDITOR", "vi")

        with tempfile.NamedTemporaryFile(mode="w+", suffix=".txt", delete=False) as tf:
            tf.write(current_content)
            tf.flush()
            temp_path = tf.name

        try:
            result = subprocess.run([editor, temp_path])

            if result.returncode != 0:
                if not self.quiet:
                    print(
                        "[GPMASTER:] Editor exited with error, not saving",
                        file=sys.stderr,
                    )
                os.unlink(temp_path)
                return

            with open(temp_path, "r") as f:
                new_content = f.read()

            if new_content == current_content:
                if not self.quiet:
                    print("[GPMASTER:] No changes made")
                os.unlink(temp_path)
                return

            data_to_sign = new_content.encode("utf-8")
            success, signature = self.gpg.sign(data_to_sign, fmt.key_id, retry=True)

            if not success:
                if not self.quiet:
                    print(
                        "[GPMASTER:] Failed to sign note, not saving changes",
                        file=sys.stderr,
                    )
                os.unlink(temp_path)
                return

            fmt.note_content = new_content
            fmt.note_signature = signature

            self._save_lockbox(fmt)

            if not self.quiet:
                print("[GPMASTER:] Note saved and signed")

        finally:
            if os.path.exists(temp_path):
                os.unlink(temp_path)

    def rekey(self, new_key_id: str):
        """Change encryption key."""
        fmt = self._load_format()

        if not self.quiet:
            print(f"[GPMASTER:] Current key: {fmt.key_id}")

        success, secrets, dec_key = self._decrypt_data(fmt, retry=not self.quiet)
        if not success:
            raise RuntimeError("Failed to decrypt lockbox")

        if not self.quiet:
            print(f"[GPMASTER:] Decrypted with key: {dec_key}")
            print(f"[GPMASTER:] Re-encrypting with key: {new_key_id}")

        secrets_json = json.dumps(secrets, separators=(",", ":")).encode("utf-8")
        success, encrypted = self.gpg.encrypt(
            secrets_json, new_key_id, retry=not self.quiet
        )
        if not success:
            raise RuntimeError("[GPMASTER:] Failed to encrypt with new key")

        fmt.key_id = new_key_id
        fmt.encrypted_data = encrypted

        data_to_sign = fmt.key_id.encode("utf-8") + encrypted
        success, signature = self.gpg.sign(data_to_sign, new_key_id, retry=True)
        if success:
            fmt.signature = signature
        else:
            fmt.signature = None

        self._save_lockbox(fmt)

        # Update agent cache with new plaintext secrets (best-effort)
        if getattr(self, "use_agent", True) and agent_client is not None:
            try:
                agent_client.set_cached(str(self.path), secrets_json)
            except Exception:
                pass

        if not self.quiet:
            print(f"[GPMASTER:] Lockbox re-keyed to: {new_key_id}")

    def dump_secrets(self, format: str = "list") -> Dict:
        """Dump all secrets (non-TOTP form)."""
        fmt = self._load_format()

        if not self.quiet:
            print(f"[GPMASTER:] Encrypted with key: {fmt.key_id}", file=sys.stderr)

        success, secrets, dec_key = self._decrypt_data(fmt, retry=not self.quiet)
        if not success:
            raise RuntimeError("[GPMASTER:] Failed to decrypt lockbox")

        if not self.quiet:
            print(f"[GPMASTER:] Decrypted with key: {dec_key}", file=sys.stderr)

        result = {}
        for name, entry in secrets.items():
            result[name] = entry["value"]

        return result

    def _is_file_secret(self, name: str) -> bool:
        """Check if a secret key represents a file."""
        return name.startswith("_FILE_")

    def _extract_filename(self, key: str) -> str:
        """Extract original filename from file secret key."""
        if self._is_file_secret(key):
            return key[6:]  # Remove "_FILE_" prefix
        return key

    def add_file(
        self,
        file_path: str,
        keep_source: bool = False,
        auto_create_key: Optional[str] = None,
    ):
        """Add a file to the lockbox."""
        file_path_obj = Path(file_path)
        if not file_path_obj.exists():
            raise FileNotFoundError(f"[GPMASTER:] File not found: {file_path}")

        filename = file_path_obj.name
        secret_key = f"_FILE_{filename}"

        with open(file_path, "rb") as f:
            file_data = f.read()

        file_data_b64 = base64.b64encode(file_data).decode("utf-8")

        # Load or create lockbox
        if self.path.exists():
            fmt = self._load_format()
            success, secrets, dec_key = self._decrypt_data(fmt, retry=not self.quiet)
            if not success:
                raise RuntimeError("[GPMASTER:] Failed to decrypt lockbox")

            if not self.quiet:
                print(f"[GPMASTER:] Decrypted with key: {dec_key}")
        else:
            if not auto_create_key:
                raise FileNotFoundError(
                    "[GPMASTER:] Lockbox does not exist. Create it first with 'gpmaster create <key-id>' or set GPMASTER_KEY_ID environment variable."
                )

            if not self.quiet:
                print(f"[GPMASTER:] Creating new lockbox with key: {auto_create_key}")
            self.create(auto_create_key)

            fmt = self._load_format()
            secrets = {}

        entry = {"value": file_data_b64, "type": "file"}
        secrets[secret_key] = entry

        if secret_key not in fmt.titles:
            fmt.titles.append(secret_key)

        secrets_json = json.dumps(secrets, separators=(",", ":")).encode("utf-8")
        success, encrypted = self.gpg.encrypt(
            secrets_json, fmt.key_id, retry=not self.quiet
        )
        if not success:
            raise RuntimeError("[GPMASTER:] Failed to encrypt lockbox")

        fmt.encrypted_data = encrypted

        data_to_sign = fmt.key_id.encode("utf-8") + encrypted
        success, signature = self.gpg.sign(data_to_sign, fmt.key_id, retry=True)
        if success:
            fmt.signature = signature

        self._save_lockbox(fmt)

        # Update agent cache with new plaintext secrets (best-effort)
        if getattr(self, "use_agent", True) and agent_client is not None:
            try:
                agent_client.set_cached(str(self.path), secrets_json)
            except Exception:
                pass

        if not keep_source:
            os.unlink(file_path)
            if not self.quiet:
                print(f"[GPMASTER:] Added file: {filename} (source removed)")
        else:
            if not self.quiet:
                print(f"[GPMASTER:] Added file: {filename}")

    def remove_file(self, filename: str):
        """Remove a file from the lockbox."""
        secret_key = f"_FILE_{filename}"

        fmt = self._load_format()
        success, secrets, dec_key = self._decrypt_data(fmt, retry=not self.quiet)
        if not success:
            raise RuntimeError("[GPMASTER:] Failed to decrypt lockbox")

        if not self.quiet:
            print(f"[GPMASTER:] Decrypted with key: {dec_key}")

        if secret_key not in secrets:
            raise KeyError(f"[GPMASTER:] File not found: {filename}")

        del secrets[secret_key]

        if secret_key in fmt.titles:
            fmt.titles.remove(secret_key)

        secrets_json = json.dumps(secrets, separators=(",", ":")).encode("utf-8")
        success, encrypted = self.gpg.encrypt(
            secrets_json, fmt.key_id, retry=not self.quiet
        )
        if not success:
            raise RuntimeError("[GPMASTER:] Failed to encrypt lockbox")

        fmt.encrypted_data = encrypted

        data_to_sign = fmt.key_id.encode("utf-8") + encrypted
        success, signature = self.gpg.sign(data_to_sign, fmt.key_id, retry=True)
        if success:
            fmt.signature = signature

        self._save_lockbox(fmt)

        # Update agent cache with new plaintext secrets (best-effort)
        if getattr(self, "use_agent", True) and agent_client is not None:
            try:
                agent_client.set_cached(str(self.path), secrets_json)
            except Exception:
                pass

        if not self.quiet:
            print(f"[GPMASTER:] Deleted file: {filename}")

    def list_files(self) -> List[str]:
        """List all files in the lockbox."""
        fmt = self._load_format()
        files = [
            self._extract_filename(title)
            for title in fmt.titles
            if self._is_file_secret(title)
        ]
        return files

    def get_file(self, filename: str) -> Optional[bytes]:
        """Retrieve file data from lockbox."""
        secret_key = f"_FILE_{filename}"

        fmt = self._load_format()

        if not self.quiet:
            print(f"[GPMASTER:] Encrypted with key: {fmt.key_id}")

        success, secrets, dec_key = self._decrypt_data(fmt, retry=not self.quiet)
        if not success:
            raise RuntimeError("[GPMASTER:] Failed to decrypt lockbox")

        if not self.quiet:
            print(f"[GPMASTER:] Decrypted with key: {dec_key}")

        if secret_key not in secrets:
            return None

        entry = secrets[secret_key]
        if entry.get("type") != "file":
            return None

        file_data_b64 = entry["value"]
        return base64.b64decode(file_data_b64)

    def get_file_list(self) -> List[str]:
        """Get list of file keys for completion."""
        try:
            fmt = self._load_format()
            return [title for title in fmt.titles if self._is_file_secret(title)]
        except Exception:
            return []
