from __future__ import annotations

import os
import re
import shutil
import subprocess
from datetime import datetime
from logger import get_logger

log = get_logger("actions.backup")

DEV_HOME = os.environ.get("WOLFPANEL_DEV_HOME")
if DEV_HOME:
    BACKUP_BASE_DIR = os.path.realpath(os.path.join(DEV_HOME, "var", "backups", "wolfpanel"))
else:
    BACKUP_BASE_DIR = os.path.realpath("/var/backups/wolfpanel")


def _verify_safe_path(backup_path: str) -> str:
    """Ensure the path is strictly within the allowed BACKUP_BASE_DIR."""
    if not backup_path:
        raise ValueError("backup_path is required")

    resolved_path = os.path.realpath(backup_path)
    base_resolved = os.path.realpath(BACKUP_BASE_DIR)

    if resolved_path != base_resolved and not resolved_path.startswith(base_resolved + os.sep):
        raise PermissionError(f"Access denied: Path is outside of backup base directory ({resolved_path})")

    return resolved_path


def _verify_docroot(docroot: str) -> str:
    """Ensure the docroot is strictly inside allowed folders (like var/www or dev var/www)."""
    if not docroot:
        raise ValueError("docroot is required")
        
    resolved = os.path.realpath(docroot)
    if DEV_HOME:
        allowed_base = os.path.realpath(os.path.join(DEV_HOME, "var", "www"))
    else:
        allowed_base = os.path.realpath("/var/www")
        
    if resolved != allowed_base and not resolved.startswith(allowed_base + os.sep):
        raise PermissionError(f"Access denied: Docroot is outside allowed folder ({resolved})")
        
    return resolved


def handle_backup_create(payload: dict) -> dict:
    """Create a backup of website files and/or database securely."""
    tmp_dir = None
    try:
        domain = payload.get("domain", "")
        docroot = payload.get("docroot", "")
        include_files = payload.get("include_files", True)
        include_db = payload.get("include_db", False)
        
        db_name = payload.get("db_name", "")
        db_user = payload.get("db_user", "")
        db_password = payload.get("db_password", "")

        # Domain validation
        if not domain or not re.match(r"^[a-zA-Z0-9.-]+$", domain):
            return {
                "success": False,
                "error": "Invalid domain name format"
            }

        backup_dir = os.path.join(BACKUP_BASE_DIR, domain)
        os.makedirs(backup_dir, exist_ok=True)
        _verify_safe_path(backup_dir)

        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"{domain}_{timestamp}.tar.gz"
        final_backup_path = os.path.join(backup_dir, filename)

        # Create temporary working directory
        tmp_dir = os.path.join(backup_dir, f"tmp_{timestamp}")
        os.makedirs(tmp_dir, exist_ok=True)

        if include_files:
            if not docroot:
                return {"success": False, "error": "docroot is required to backup files"}
            resolved_docroot = _verify_docroot(docroot)
            if not os.path.exists(resolved_docroot):
                return {"success": False, "error": f"docroot directory not found: {docroot}"}
            
            # Archive website files inside tmp
            files_archive_path = os.path.join(tmp_dir, "files.tar.gz")
            subprocess.run(
                ["tar", "-czf", files_archive_path, "-C", resolved_docroot, "."],
                capture_output=True,
                check=True
            )

        if include_db:
            if not db_name or not db_user:
                return {"success": False, "error": "db_name and db_user are required to backup database"}
            
            # Dump SQL and compress it
            sql_path = os.path.join(tmp_dir, "db.sql")
            env = os.environ.copy()
            if db_password:
                env["MYSQL_PWD"] = db_password

            # Run mysqldump
            with open(sql_path, "wb") as sql_file:
                res = subprocess.run(
                    ["mysqldump", "-u", db_user, "-h", "127.0.0.1", db_name],
                    env=env,
                    stdout=sql_file,
                    stderr=subprocess.PIPE,
                    check=False
                )
                if res.returncode != 0:
                    error_msg = res.stderr.decode("utf-8", errors="ignore")
                    return {"success": False, "error": f"Database dump failed: {error_msg}"}

            # gzip the sql dump
            subprocess.run(["gzip", sql_path], check=True)

        # Create final archive containing the components
        subprocess.run(
            ["tar", "-czf", final_backup_path, "-C", tmp_dir, "."],
            capture_output=True,
            check=True
        )

        # Get size
        size_bytes = os.path.getsize(final_backup_path)

        # Apply backup retention
        try:
            from config import load_config
            config = load_config()
            retention = getattr(config, "backup_retention", 5)
            # Find and sort all backup files for this domain
            all_backups = []
            for name in os.listdir(backup_dir):
                if name.endswith(".tar.gz") and not name.startswith("tmp_"):
                    full_path = os.path.join(backup_dir, name)
                    if os.path.isfile(full_path):
                        all_backups.append((full_path, os.path.getmtime(full_path)))
            
            # Sort by mtime ascending (oldest first)
            all_backups.sort(key=lambda x: x[1])
            
            # If we exceed retention, delete the oldest ones
            if len(all_backups) > retention:
                to_delete = all_backups[:-retention]
                for path, _ in to_delete:
                    try:
                        os.remove(path)
                        log.info("Deleted old backup file according to retention: %s", path)
                    except Exception as e:
                        log.warning("Failed to delete old backup file %s: %s", path, e)
        except Exception as e:
            log.warning("Failed to apply backup retention: %s", e)

        return {
            "success": True,
            "backup_path": final_backup_path,
            "filename": filename,
            "size_bytes": size_bytes,
            "created_at": datetime.now().isoformat()
        }

    except Exception as exc:
        log.error("Backup creation failed: %s", exc)
        return {
            "success": False,
            "error": str(exc)
        }
    finally:
        if tmp_dir and os.path.exists(tmp_dir):
            try:
                shutil.rmtree(tmp_dir)
            except Exception:
                pass


def handle_backup_delete(payload: dict) -> dict:
    """Delete a backup archive file securely."""
    try:
        backup_path = payload.get("backup_path", "")
        resolved_path = _verify_safe_path(backup_path)

        if not os.path.exists(resolved_path) or not os.path.isfile(resolved_path):
            return {
                "success": False,
                "error": f"Backup file not found: {backup_path}"
            }

        os.remove(resolved_path)
        return {
            "success": True,
            "deleted": True
        }
    except Exception as exc:
        log.error("Backup deletion failed: %s", exc)
        return {
            "success": False,
            "error": str(exc)
        }


def handle_backup_list(payload: dict) -> dict:
    """List backup archives for a domain."""
    try:
        domain = payload.get("domain", "")
        if not domain or not re.match(r"^[a-zA-Z0-9.-]+$", domain):
            return {
                "success": False,
                "error": "Invalid domain name format"
            }

        backup_dir = os.path.join(BACKUP_BASE_DIR, domain)
        if not os.path.exists(backup_dir):
            return {
                "success": True,
                "backups": []
            }

        _verify_safe_path(backup_dir)

        backups = []
        for name in os.listdir(backup_dir):
            if not name.endswith(".tar.gz") or "tmp_" in name:
                continue
            
            full_path = os.path.join(backup_dir, name)
            if not os.path.isfile(full_path):
                continue
                
            stat = os.stat(full_path)
            
            backups.append({
                "filename": name,
                "size_bytes": stat.st_size,
                "created_at": datetime.fromtimestamp(stat.st_mtime).isoformat(),
                "path": full_path
            })

        # Sort newest first
        backups.sort(key=lambda x: x["created_at"], reverse=True)

        return {
            "success": True,
            "backups": backups
        }
    except Exception as exc:
        log.error("Backup listing failed: %s", exc)
        return {
            "success": False,
            "error": str(exc)
        }


def handle_backup_restore(payload: dict) -> dict:
    """Restore website files and/or database from a backup securely."""
    tmp_dir = None
    try:
        backup_path = payload.get("backup_path", "")
        docroot = payload.get("docroot", "")
        restore_files = payload.get("restore_files", True)
        restore_db = payload.get("restore_db", False)
        
        db_name = payload.get("db_name", "")
        db_user = payload.get("db_user", "")
        db_password = payload.get("db_password", "")

        resolved_backup = _verify_safe_path(backup_path)
        if not os.path.exists(resolved_backup) or not os.path.isfile(resolved_backup):
            return {
                "success": False,
                "error": f"Backup file not found: {backup_path}"
            }

        # Create temp folder to extract main archive
        parent_dir = os.path.dirname(resolved_backup)
        tmp_dir = os.path.join(parent_dir, f"restore_tmp_{int(datetime.now().timestamp())}")
        os.makedirs(tmp_dir, exist_ok=True)

        # Extract main backup tarball to tmp
        subprocess.run(
            ["tar", "-xzf", resolved_backup, "-C", tmp_dir],
            capture_output=True,
            check=True
        )

        if restore_files:
            if not docroot:
                return {"success": False, "error": "docroot is required to restore files"}
            resolved_docroot = _verify_docroot(docroot)
            os.makedirs(resolved_docroot, exist_ok=True)
            
            files_archive = os.path.join(tmp_dir, "files.tar.gz")
            if os.path.exists(files_archive):
                subprocess.run(
                    ["tar", "-xzf", files_archive, "-C", resolved_docroot],
                    capture_output=True,
                    check=True
                )
            else:
                log.warning("files.tar.gz not found in backup archive")

        if restore_db:
            if not db_name or not db_user:
                return {"success": False, "error": "db_name and db_user are required to restore database"}
                
            sql_gz = os.path.join(tmp_dir, "db.sql.gz")
            sql_path = os.path.join(tmp_dir, "db.sql")
            
            if os.path.exists(sql_gz):
                # Gunzip it
                subprocess.run(["gunzip", "-f", sql_gz], check=True)
                
            if os.path.exists(sql_path):
                env = os.environ.copy()
                if db_password:
                    env["MYSQL_PWD"] = db_password
                    
                # Run mysql restore
                with open(sql_path, "rb") as sql_file:
                    res = subprocess.run(
                        ["mysql", "-u", db_user, "-h", "127.0.0.1", db_name],
                        env=env,
                        stdin=sql_file,
                        stderr=subprocess.PIPE,
                        check=False
                    )
                    if res.returncode != 0:
                        error_msg = res.stderr.decode("utf-8", errors="ignore")
                        return {"success": False, "error": f"Database restore failed: {error_msg}"}
            else:
                log.warning("db.sql not found in backup archive")

        return {
            "success": True,
            "restored": True
        }

    except Exception as exc:
        log.error("Backup restoration failed: %s", exc)
        return {
            "success": False,
            "error": str(exc)
        }
    finally:
        if tmp_dir and os.path.exists(tmp_dir):
            try:
                shutil.rmtree(tmp_dir)
            except Exception:
                pass
