"""File write executor with security controls."""

import os
import tempfile
import time
from pathlib import Path
from typing import Any

from app.config import get_settings
from app.executors.base import BaseExecutor, ExecutionResult
from app.utils.validation import ValidationError, sanitize_input, validate_file_path


class FileExecutor(BaseExecutor):
    """Write files with strict security controls.

    Security measures:
    - Path validation against allowed root directories
    - Directory traversal prevention
    - Maximum file size enforcement
    - Atomic writes (temp file + rename)
    - Content sanitization

    Supported roots:
    - /opt/agent_data (general file operations)
    - /opt/letsbe/env (ENV file operations)

    Payload:
        {
            "path": "/opt/letsbe/env/app.env",
            "content": "KEY=value\\nKEY2=value2",
            "mode": "write"         # "write" (default) or "append"
        }

    Result:
        {
            "written": true,
            "path": "/opt/letsbe/env/app.env",
            "size": 123
        }
    """

    @property
    def task_type(self) -> str:
        return "FILE_WRITE"

    async def execute(self, payload: dict[str, Any]) -> ExecutionResult:
        """Write content to a file.

        Args:
            payload: Must contain "path" and "content", optionally "mode"

        Returns:
            ExecutionResult with write confirmation
        """
        self.validate_payload(payload, ["path", "content"])
        settings = get_settings()

        file_path = payload["path"]
        content = payload["content"]
        mode = payload.get("mode", "write")

        if mode not in ("write", "append"):
            return ExecutionResult(
                success=False,
                data={},
                error=f"Invalid mode: {mode}. Must be 'write' or 'append'",
            )

        # Validate path against allowed roots (env or general)
        # Try env root first if path starts with it, otherwise use general root
        try:
            allowed_root = self._determine_allowed_root(file_path, settings)
            validated_path = validate_file_path(
                file_path,
                allowed_root,
                must_exist=False,
            )
            sanitized_content = sanitize_input(content, max_length=settings.max_file_size)
        except ValidationError as e:
            self.logger.warning("file_validation_failed", path=file_path, error=str(e))
            return ExecutionResult(
                success=False,
                data={},
                error=f"Validation failed: {e}",
            )

        # Check content size
        content_bytes = sanitized_content.encode("utf-8")
        if len(content_bytes) > settings.max_file_size:
            return ExecutionResult(
                success=False,
                data={},
                error=f"Content size {len(content_bytes)} exceeds max {settings.max_file_size}",
            )

        self.logger.info(
            "file_writing",
            path=str(validated_path),
            mode=mode,
            size=len(content_bytes),
        )

        start_time = time.time()

        try:
            if mode == "write":
                bytes_written = await self._atomic_write(validated_path, content_bytes)
            else:
                bytes_written = await self._append(validated_path, content_bytes)

            duration_ms = (time.time() - start_time) * 1000

            self.logger.info(
                "file_written",
                path=str(validated_path),
                bytes_written=bytes_written,
                duration_ms=duration_ms,
            )

            return ExecutionResult(
                success=True,
                data={
                    "written": True,
                    "path": str(validated_path),
                    "size": bytes_written,
                },
                duration_ms=duration_ms,
            )

        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            self.logger.error("file_write_error", path=str(validated_path), error=str(e))
            return ExecutionResult(
                success=False,
                data={},
                error=str(e),
                duration_ms=duration_ms,
            )

    def _determine_allowed_root(self, file_path: str, settings) -> str:
        """Determine which allowed root to use based on file path.

        Args:
            file_path: The requested file path
            settings: Application settings

        Returns:
            The appropriate allowed root directory
        """
        from pathlib import Path as P

        # Normalize the path for comparison
        normalized = str(P(file_path).expanduser())

        # Check if path is under env root
        env_root = str(P(settings.allowed_env_root).expanduser())
        if normalized.startswith(env_root):
            return settings.allowed_env_root

        # Default to general file root
        return settings.allowed_file_root

    async def _atomic_write(self, path: Path, content: bytes) -> int:
        """Write file atomically using temp file + rename.

        Args:
            path: Target file path
            content: Content to write

        Returns:
            Number of bytes written
        """
        import asyncio

        def _write() -> int:
            # Ensure parent directory exists
            path.parent.mkdir(parents=True, exist_ok=True)

            # Write to temp file in same directory (for atomic rename)
            fd, temp_path = tempfile.mkstemp(
                dir=path.parent,
                prefix=".tmp_",
                suffix=path.suffix,
            )

            try:
                os.write(fd, content)
                os.fsync(fd)  # Ensure data is on disk
            finally:
                os.close(fd)

            # Atomic rename
            os.rename(temp_path, path)

            return len(content)

        return await asyncio.to_thread(_write)

    async def _append(self, path: Path, content: bytes) -> int:
        """Append content to file.

        Args:
            path: Target file path
            content: Content to append

        Returns:
            Number of bytes written
        """
        import asyncio

        def _append() -> int:
            # Ensure parent directory exists
            path.parent.mkdir(parents=True, exist_ok=True)

            with open(path, "ab") as f:
                written = f.write(content)
                f.flush()
                os.fsync(f.fileno())

            return written

        return await asyncio.to_thread(_append)
