from __future__ import annotations

import mimetypes
import os
import shutil
from datetime import datetime
from logger import get_logger

log = get_logger("actions.file_manager")

DEV_HOME = os.environ.get("WOLFPANEL_DEV_HOME")
if DEV_HOME:
    BASE_PATH = os.path.realpath(os.path.join(DEV_HOME, "var", "www"))
else:
    BASE_PATH = os.path.realpath("/var/www")


def _verify_safe_path(user_path: str) -> str:
    """Normalize and verify that the path is strictly within the allowed BASE_PATH."""
    if not user_path:
        raise ValueError("path is required")

    # If the user path is not absolute, treat it as relative to BASE_PATH
    if not os.path.isabs(user_path):
        resolved_path = os.path.realpath(os.path.join(BASE_PATH, user_path.lstrip("/")))
    else:
        resolved_path = os.path.realpath(user_path)

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

    return resolved_path


def _format_size(size_bytes: int) -> str:
    """Format file sizes into human-readable strings."""
    if size_bytes < 1024:
        return f"{size_bytes} B"
    elif size_bytes < 1024 * 1024:
        return f"{size_bytes / 1024:.1f} KB"
    else:
        return f"{size_bytes / (1024 * 1024):.1f} MB"


def handle_file_list(payload: dict) -> dict:
    """List directory contents under the verified path."""
    try:
        user_path = payload.get("path", "")
        resolved_path = _verify_safe_path(user_path)

        if not os.path.exists(resolved_path):
            return {
                "success": False,
                "error": f"Directory not found: {user_path}"
            }
        if not os.path.isdir(resolved_path):
            return {
                "success": False,
                "error": f"Path is not a directory: {user_path}"
            }

        items = []
        for name in os.listdir(resolved_path):
            # Skip current and parent directories (normally not in listdir anyway)
            if name in (".", ".."):
                continue

            full_path = os.path.join(resolved_path, name)
            is_dir = os.path.isdir(full_path)
            
            try:
                stat = os.stat(full_path)
                size_str = _format_size(stat.st_size) if not is_dir else None
                mtime_str = datetime.fromtimestamp(stat.st_mtime).strftime("%d.%m.%Y %H:%M")
            except Exception:
                size_str = "0 B" if not is_dir else None
                mtime_str = "Unknown"

            items.append({
                "name": name,
                "type": "folder" if is_dir else "file",
                "size": size_str,
                "modified_at": mtime_str
            })

        return {
            "success": True,
            "items": items
        }
    except Exception as exc:
        log.error("File list failed: %s", exc)
        return {
            "success": False,
            "error": str(exc)
        }


def handle_file_read(payload: dict) -> dict:
    """Read textual files safely (max 2MB, rejects binary)."""
    try:
        user_path = payload.get("path", "")
        resolved_path = _verify_safe_path(user_path)

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

        # Size check (2MB)
        file_size = os.path.getsize(resolved_path)
        if file_size > 2 * 1024 * 1024:
            return {
                "success": False,
                "error": "File exceeds maximum allowed size of 2MB"
            }

        # Binary check via null byte in the first chunk
        with open(resolved_path, "rb") as f:
            chunk = f.read(1024)
            if b"\x00" in chunk:
                return {
                    "success": False,
                    "error": "Binary files are not allowed"
                }

        # Binary check via mimetype guess
        mime_type, _ = mimetypes.guess_type(resolved_path)
        if mime_type:
            is_text = mime_type.startswith("text/") or mime_type in (
                "application/json",
                "application/javascript",
                "application/xml",
                "application/x-javascript",
                "application/x-sh",
                "application/x-bash",
                "application/x-php",
                "application/xhtml+xml"
            )
            if not is_text:
                return {
                    "success": False,
                    "error": "Binary files are not allowed"
                }

        with open(resolved_path, "r", encoding="utf-8", errors="ignore") as f:
            content = f.read()

        return {
            "success": True,
            "content": content,
            "encoding": "utf-8"
        }
    except Exception as exc:
        log.error("File read failed: %s", exc)
        return {
            "success": False,
            "error": str(exc)
        }


def handle_file_write(payload: dict) -> dict:
    """Write text files atomically (max 2MB)."""
    tmp_path = None
    try:
        user_path = payload.get("path", "")
        content = payload.get("content", "")

        resolved_path = _verify_safe_path(user_path)

        # Ensure parent folder exists
        os.makedirs(os.path.dirname(resolved_path), exist_ok=True)

        # Check content size
        content_bytes = content.encode("utf-8")
        if len(content_bytes) > 2 * 1024 * 1024:
            return {
                "success": False,
                "error": "Content exceeds maximum allowed size of 2MB"
            }

        # Write atomically
        tmp_path = resolved_path + ".tmp"
        with open(tmp_path, "wb") as f:
            f.write(content_bytes)

        os.replace(tmp_path, resolved_path)
        return {
            "success": True,
            "written": True
        }
    except Exception as exc:
        log.error("File write failed: %s", exc)
        if tmp_path and os.path.exists(tmp_path):
            try:
                os.remove(tmp_path)
            except Exception:
                pass
        return {
            "success": False,
            "error": str(exc)
        }


def handle_file_delete(payload: dict) -> dict:
    """Delete a file or directory."""
    try:
        user_path = payload.get("path", "")
        resolved_path = _verify_safe_path(user_path)

        if not os.path.exists(resolved_path):
            return {
                "success": False,
                "error": f"Path not found: {user_path}"
            }

        if os.path.isdir(resolved_path):
            shutil.rmtree(resolved_path)
        else:
            os.remove(resolved_path)

        return {
            "success": True,
            "deleted": True
        }
    except Exception as exc:
        log.error("File delete failed: %s", exc)
        return {
            "success": False,
            "error": str(exc)
        }


def handle_file_rename(payload: dict) -> dict:
    """Rename a file or directory inside its folder."""
    try:
        old_path = payload.get("old_path", "")
        new_name = payload.get("new_name", "")

        if not old_path or not new_name:
            return {
                "success": False,
                "error": "Both old_path and new_name are required"
            }

        # Prevent directory traversal in new_name
        if "/" in new_name or "\\" in new_name:
            return {
                "success": False,
                "error": "Invalid file name format"
            }

        old_resolved = _verify_safe_path(old_path)
        parent_dir = os.path.dirname(old_resolved)
        new_resolved = _verify_safe_path(os.path.join(parent_dir, new_name))

        if not os.path.exists(old_resolved):
            return {
                "success": False,
                "error": f"Path not found: {old_path}"
            }

        # Prevent overwriting an existing file/folder
        if os.path.exists(new_resolved):
            return {
                "success": False,
                "error": "A file or folder with the new name already exists"
            }

        os.rename(old_resolved, new_resolved)
        return {
            "success": True,
            "renamed": True
        }
    except Exception as exc:
        log.error("File rename failed: %s", exc)
        return {
            "success": False,
            "error": str(exc)
        }


def handle_file_create(payload: dict) -> dict:
    """Create a blank file or directory."""
    try:
        user_path = payload.get("path", "")
        item_type = payload.get("type", "file")

        resolved_path = _verify_safe_path(user_path)

        # Ensure parent folder exists
        os.makedirs(os.path.dirname(resolved_path), exist_ok=True)

        if os.path.exists(resolved_path):
            return {
                "success": False,
                "error": "Path already exists"
            }

        if item_type == "folder":
            os.makedirs(resolved_path, exist_ok=True)
        else:
            with open(resolved_path, "w", encoding="utf-8") as f:
                pass

        return {
            "success": True,
            "created": True
        }
    except Exception as exc:
        log.error("File create failed: %s", exc)
        return {
            "success": False,
            "error": str(exc)
        }
