"""
BizNode Backup Service
======================
Handles scheduled, encrypted backups to local storage and cloud providers.

Providers: local | google_drive | dropbox | s3 | nas
Encryption: Fernet (AES-128-CBC + HMAC-SHA256)
Key file:   backups/.backup.key  (never uploaded to cloud)
"""

from __future__ import annotations

import importlib.util
import json
import logging
import os
import shutil
import stat
import zipfile
from datetime import datetime, timezone
from hashlib import sha256
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

log = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------

_PROJECT_ROOT = Path(__file__).resolve().parent.parent
_BACKUP_DIR   = _PROJECT_ROOT / "backups"
_KEY_FILE     = _BACKUP_DIR / ".backup.key"

# Targets: (source_relative_to_project_root, archive_internal_path)
BACKUP_TARGETS: List[Tuple[str, str]] = [
    ("memory/biznode.db", "data/biznode.db"),
    ("identity",          "identity"),
    (".env",              "config/.env"),
    ("config",            "config/node"),
]
OPTIONAL_TARGETS: List[Tuple[str, str]] = [
    ("qdrant_storage", "data/qdrant"),
]


# ---------------------------------------------------------------------------
# DB helper (importlib to avoid __init__ heavy deps)
# ---------------------------------------------------------------------------

def _load_db():
    db_path = _PROJECT_ROOT / "memory" / "database.py"
    spec = importlib.util.spec_from_file_location("_db", str(db_path))
    mod  = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


# ---------------------------------------------------------------------------
# Fernet key management
# ---------------------------------------------------------------------------

def get_or_create_fernet_key() -> bytes:
    """Load or generate the Fernet backup key from backups/.backup.key."""
    from cryptography.fernet import Fernet

    _BACKUP_DIR.mkdir(parents=True, exist_ok=True)

    if _KEY_FILE.exists():
        return _KEY_FILE.read_bytes().strip()

    key = Fernet.generate_key()
    _KEY_FILE.write_bytes(key)
    try:
        _KEY_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR)  # 0o600
    except Exception:
        pass
    log.info("Generated new Fernet backup key: %s", _KEY_FILE)
    return key


def get_key_fingerprint() -> str:
    """Return first 16 hex chars of SHA256(key) for display in UI."""
    key = get_or_create_fernet_key()
    return sha256(key).hexdigest()[:16]


def encrypt_config(d: dict) -> str:
    """Fernet-encrypt a provider config dict for DB storage."""
    from cryptography.fernet import Fernet
    key = get_or_create_fernet_key()
    f   = Fernet(key)
    return f.encrypt(json.dumps(d).encode()).decode()


def decrypt_config(s: str) -> dict:
    """Reverse of encrypt_config."""
    from cryptography.fernet import Fernet
    key = get_or_create_fernet_key()
    f   = Fernet(key)
    return json.loads(f.decrypt(s.encode()).decode())


# ---------------------------------------------------------------------------
# Archive creation & encryption
# ---------------------------------------------------------------------------

def create_backup_archive() -> Path:
    """
    Zip all BACKUP_TARGETS (+ optional ones if present) into a timestamped .zip.
    Returns the Path to the created .zip inside backups/.
    """
    _BACKUP_DIR.mkdir(parents=True, exist_ok=True)
    ts       = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
    zip_path = _BACKUP_DIR / f"biznode_backup_{ts}.zip"

    with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
        targets = BACKUP_TARGETS + OPTIONAL_TARGETS
        for rel_src, arc_dest in targets:
            src = _PROJECT_ROOT / rel_src
            if not src.exists():
                log.debug("Backup target missing, skipping: %s", src)
                continue
            if src.is_file():
                zf.write(src, arc_dest)
            else:
                for file in src.rglob("*"):
                    if file.is_file():
                        arcname = arc_dest + "/" + str(file.relative_to(src))
                        zf.write(file, arcname)

    log.info("Created backup archive: %s (%d bytes)", zip_path.name, zip_path.stat().st_size)
    return zip_path


def encrypt_archive(zip_path: Path) -> Path:
    """
    Fernet-encrypt zip_path → .enc file.  Deletes the .zip when done.
    Returns the Path to the .enc file.
    """
    from cryptography.fernet import Fernet
    key      = get_or_create_fernet_key()
    f        = Fernet(key)
    enc_path = zip_path.with_suffix(".enc")
    data     = zip_path.read_bytes()
    enc_path.write_bytes(f.encrypt(data))
    zip_path.unlink()
    log.info("Encrypted archive: %s (%d bytes)", enc_path.name, enc_path.stat().st_size)
    return enc_path


def decrypt_archive(enc_path: Path, dest_dir: Path) -> Path:
    """
    Decrypt a .enc backup archive and extract it to dest_dir.
    Returns the Path to the extracted directory.
    """
    from cryptography.fernet import Fernet
    import io
    key  = get_or_create_fernet_key()
    f    = Fernet(key)
    data = f.decrypt(enc_path.read_bytes())
    dest_dir.mkdir(parents=True, exist_ok=True)
    with zipfile.ZipFile(io.BytesIO(data)) as zf:
        zf.extractall(dest_dir)
    log.info("Decrypted and extracted %s → %s", enc_path.name, dest_dir)
    return dest_dir


# ---------------------------------------------------------------------------
# Retention helper
# ---------------------------------------------------------------------------

def _apply_retention(directory: Path, retention: int) -> None:
    """Delete oldest .enc files in directory, keeping most recent `retention`."""
    enc_files = sorted(directory.glob("biznode_backup_*.enc"), key=lambda p: p.stat().st_mtime)
    to_delete  = enc_files[: max(0, len(enc_files) - retention)]
    for f in to_delete:
        try:
            f.unlink()
            log.info("Retention: deleted old backup %s", f.name)
        except Exception as exc:
            log.warning("Retention: could not delete %s: %s", f.name, exc)


# ---------------------------------------------------------------------------
# Provider: local
# ---------------------------------------------------------------------------

def backup_local(enc_path: Path, retention: int = 7) -> bool:
    """
    The encrypted archive is already in backups/; just apply retention.
    """
    try:
        _apply_retention(_BACKUP_DIR, retention)
        return True
    except Exception as exc:
        log.error("local backup failed: %s", exc)
        return False


# ---------------------------------------------------------------------------
# Provider: Google Drive
# ---------------------------------------------------------------------------

def get_google_auth_url(client_id: str, client_secret: str, redirect_uri: str) -> str:
    """Return the Google OAuth2 consent URL."""
    from google_auth_oauthlib.flow import Flow

    flow = Flow.from_client_config(
        {
            "web": {
                "client_id": client_id,
                "client_secret": client_secret,
                "redirect_uris": [redirect_uri],
                "auth_uri": "https://accounts.google.com/o/oauth2/auth",
                "token_uri": "https://oauth2.googleapis.com/token",
            }
        },
        scopes=["https://www.googleapis.com/auth/drive.file"],
        redirect_uri=redirect_uri,
    )
    url, _ = flow.authorization_url(access_type="offline", prompt="consent")
    return url


def exchange_google_code(
    client_id: str, client_secret: str, code: str, redirect_uri: str
) -> str:
    """Exchange OAuth2 code for credentials JSON string."""
    from google_auth_oauthlib.flow import Flow

    flow = Flow.from_client_config(
        {
            "web": {
                "client_id": client_id,
                "client_secret": client_secret,
                "redirect_uris": [redirect_uri],
                "auth_uri": "https://accounts.google.com/o/oauth2/auth",
                "token_uri": "https://oauth2.googleapis.com/token",
            }
        },
        scopes=["https://www.googleapis.com/auth/drive.file"],
        redirect_uri=redirect_uri,
    )
    flow.fetch_token(code=code)
    return flow.credentials.to_json()


def _gdrive_get_or_create_folder(service, folder_name: str) -> str:
    """Return folder_id for 'folder_name' in Drive root, creating if needed."""
    query = (
        f"name='{folder_name}' and mimeType='application/vnd.google-apps.folder'"
        " and trashed=false"
    )
    results = service.files().list(q=query, fields="files(id)").execute()
    files   = results.get("files", [])
    if files:
        return files[0]["id"]
    meta = {"name": folder_name, "mimeType": "application/vnd.google-apps.folder"}
    folder = service.files().create(body=meta, fields="id").execute()
    return folder["id"]


def backup_google_drive(
    enc_path: Path, token_json: str, client_id: str, client_secret: str, retention: int = 7
) -> bool:
    try:
        import google.oauth2.credentials as g_creds
        from google.auth.transport.requests import Request
        from googleapiclient.discovery import build
        from googleapiclient.http import MediaFileUpload

        token_data = json.loads(token_json)
        creds = g_creds.Credentials.from_authorized_user_info(token_data)
        if creds.expired and creds.refresh_token:
            creds.refresh(Request())
        service = build("drive", "v3", credentials=creds, cache_discovery=False)

        folder_id = _gdrive_get_or_create_folder(service, "BizNode Backups")
        media     = MediaFileUpload(str(enc_path), resumable=False)
        service.files().create(
            body={"name": enc_path.name, "parents": [folder_id]},
            media_body=media,
            fields="id",
        ).execute()
        log.info("Google Drive: uploaded %s", enc_path.name)

        # Retention — list & delete oldest
        results = service.files().list(
            q=f"'{folder_id}' in parents and name contains 'biznode_backup_' and trashed=false",
            orderBy="createdTime",
            fields="files(id,name)",
        ).execute()
        all_files = results.get("files", [])
        to_delete = all_files[: max(0, len(all_files) - retention)]
        for item in to_delete:
            service.files().delete(fileId=item["id"]).execute()
            log.info("Google Drive retention: deleted %s", item["name"])

        return True
    except Exception as exc:
        log.error("Google Drive backup failed: %s", exc)
        return False


# ---------------------------------------------------------------------------
# Provider: Dropbox
# ---------------------------------------------------------------------------

def backup_dropbox(enc_path: Path, access_token: str, retention: int = 7) -> bool:
    try:
        import dropbox
        from dropbox.files import WriteMode

        dbx      = dropbox.Dropbox(access_token)
        dest     = f"/BizNode Backups/{enc_path.name}"
        data     = enc_path.read_bytes()
        dbx.files_upload(data, dest, mode=WriteMode("overwrite"))
        log.info("Dropbox: uploaded %s", enc_path.name)

        # Retention
        result = dbx.files_list_folder("/BizNode Backups")
        entries = sorted(
            [e for e in result.entries if e.name.startswith("biznode_backup_")],
            key=lambda e: e.name,
        )
        to_delete = entries[: max(0, len(entries) - retention)]
        for entry in to_delete:
            dbx.files_delete_v2(entry.path_lower)
            log.info("Dropbox retention: deleted %s", entry.name)

        return True
    except Exception as exc:
        log.error("Dropbox backup failed: %s", exc)
        return False


# ---------------------------------------------------------------------------
# Provider: AWS S3
# ---------------------------------------------------------------------------

def backup_s3(
    enc_path: Path,
    access_key: str,
    secret_key: str,
    bucket: str,
    region: str = "us-east-1",
    prefix: str = "biznode-backups/",
    retention: int = 7,
) -> bool:
    try:
        import boto3

        s3  = boto3.client(
            "s3",
            aws_access_key_id=access_key,
            aws_secret_access_key=secret_key,
            region_name=region,
        )
        key = prefix + enc_path.name
        s3.upload_file(str(enc_path), bucket, key)
        log.info("S3: uploaded s3://%s/%s", bucket, key)

        # Retention
        paginator = s3.get_paginator("list_objects_v2")
        objects   = []
        for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
            objects.extend(page.get("Contents", []))
        objects.sort(key=lambda o: o["LastModified"])
        to_delete = objects[: max(0, len(objects) - retention)]
        if to_delete:
            s3.delete_objects(
                Bucket=bucket,
                Delete={"Objects": [{"Key": o["Key"]} for o in to_delete]},
            )
            log.info("S3 retention: deleted %d old backup(s)", len(to_delete))

        return True
    except Exception as exc:
        log.error("S3 backup failed: %s", exc)
        return False


# ---------------------------------------------------------------------------
# Provider: NAS / local network path
# ---------------------------------------------------------------------------

def backup_nas(enc_path: Path, dest_path: str, retention: int = 7) -> bool:
    try:
        dest = Path(dest_path)
        dest.mkdir(parents=True, exist_ok=True)
        shutil.copy2(str(enc_path), str(dest / enc_path.name))
        log.info("NAS: copied %s → %s", enc_path.name, dest)

        _apply_retention(dest, retention)
        return True
    except Exception as exc:
        log.error("NAS backup failed: %s", exc)
        return False


# ---------------------------------------------------------------------------
# Orchestrator
# ---------------------------------------------------------------------------

def run_backup() -> Dict[str, Any]:
    """
    Orchestrate a full backup run across all enabled providers.
    Returns a summary dict.
    """
    db      = _load_db()
    configs = [c for c in db.get_all_backup_configs() if c["enabled"]]

    if not configs:
        log.info("No backup providers enabled — skipping run.")
        return {"success": False, "reason": "no providers enabled", "results": {}}

    # Create and encrypt archive
    try:
        zip_path = create_backup_archive()
        enc_path = encrypt_archive(zip_path)
    except Exception as exc:
        log.error("Failed to create backup archive: %s", exc)
        return {"success": False, "reason": str(exc), "results": {}}

    filename   = enc_path.name
    size_bytes = enc_path.stat().st_size
    now        = datetime.now(timezone.utc).isoformat()
    results: Dict[str, str] = {}

    for cfg in configs:
        provider  = cfg["provider"]
        retention = int(cfg.get("retention") or 7)
        raw_cfg   = cfg.get("config_json") or "{}"

        try:
            provider_config = decrypt_config(raw_cfg) if raw_cfg != "{}" else {}
        except Exception:
            provider_config = {}

        ok = False
        try:
            if provider == "local":
                ok = backup_local(enc_path, retention)
            elif provider == "google_drive":
                ok = backup_google_drive(
                    enc_path,
                    token_json=provider_config.get("token_json", ""),
                    client_id=provider_config.get("client_id", ""),
                    client_secret=provider_config.get("client_secret", ""),
                    retention=retention,
                )
            elif provider == "dropbox":
                ok = backup_dropbox(
                    enc_path,
                    access_token=provider_config.get("access_token", ""),
                    retention=retention,
                )
            elif provider == "s3":
                ok = backup_s3(
                    enc_path,
                    access_key=provider_config.get("access_key", ""),
                    secret_key=provider_config.get("secret_key", ""),
                    bucket=provider_config.get("bucket", ""),
                    region=provider_config.get("region", "us-east-1"),
                    prefix=provider_config.get("prefix", "biznode-backups/"),
                    retention=retention,
                )
            elif provider == "nas":
                ok = backup_nas(
                    enc_path,
                    dest_path=provider_config.get("path", ""),
                    retention=retention,
                )
            else:
                log.warning("Unknown backup provider: %s", provider)
        except Exception as exc:
            log.error("Provider %s raised: %s", provider, exc)
            ok = False

        status = "success" if ok else "failed"
        error  = None if ok else "provider returned failure"
        results[provider] = status

        db.record_backup_history(provider, filename, size_bytes, status, error)
        if ok:
            db.update_backup_last_run(provider, now)

    all_ok = all(v == "success" for v in results.values())
    return {
        "success":    all_ok,
        "filename":   filename,
        "size_bytes": size_bytes,
        "results":    results,
    }
