"""Nextcloud domain configuration executor."""

import asyncio
import subprocess
import time
from pathlib import Path
from typing import Any
from urllib.parse import urlparse

from app.executors.base import BaseExecutor, ExecutionResult


class NextcloudSetDomainExecutor(BaseExecutor):
    """Execute Nextcloud domain configuration via occ commands.

    This executor configures Nextcloud's external domain settings by running
    occ config:system:set commands via docker compose exec. It keeps the
    Orchestrator unaware of container names, occ paths, and docker-compose syntax.

    Security measures:
    - URL parsing with validation
    - No shell=True, command list only
    - Timeout enforcement on each subprocess

    Payload:
        {
            "public_url": "https://cloud.example.com"
        }

    Result (success):
        {
            "public_url": "https://cloud.example.com",
            "host": "cloud.example.com",
            "scheme": "https",
            "commands_executed": 3,
            "logs": {
                "overwritehost": "<stdout+stderr>",
                "overwriteprotocol": "<stdout+stderr>",
                "overwrite.cli.url": "<stdout+stderr>"
            }
        }

    Result (failure):
        {
            "public_url": "https://cloud.example.com",
            "host": "cloud.example.com",
            "scheme": "https",
            "commands_executed": 2,
            "failed_command": "overwriteprotocol",
            "failed_args": ["config:system:set", "overwriteprotocol", "--value=https"],
            "logs": {...}
        }
    """

    # TODO: These constants may need adjustment based on actual Nextcloud stack setup
    NEXTCLOUD_STACK_DIR = "/opt/letsbe/stacks/nextcloud"
    NEXTCLOUD_SERVICE_NAME = "app"
    NEXTCLOUD_OCC_PATH = "/var/www/html/occ"
    NEXTCLOUD_USER = "www-data"

    # Compose file search order (matches DockerExecutor)
    COMPOSE_FILE_NAMES = ["docker-compose.yml", "compose.yml"]

    # Default timeout for each occ command (seconds)
    DEFAULT_COMMAND_TIMEOUT = 60

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

    async def execute(self, payload: dict[str, Any]) -> ExecutionResult:
        """Execute Nextcloud domain configuration commands.

        Runs three occ config:system:set commands to configure:
        - overwritehost: The domain/host portion of the URL
        - overwriteprotocol: The scheme (http/https)
        - overwrite.cli.url: The full public URL

        Args:
            payload: Must contain "public_url", optionally "timeout"

        Returns:
            ExecutionResult with configuration confirmation and logs
        """
        self.validate_payload(payload, ["public_url"])

        public_url = payload["public_url"]
        timeout = payload.get("timeout", self.DEFAULT_COMMAND_TIMEOUT)

        # Parse URL into components
        try:
            scheme, host, normalized_url = self._parse_public_url(public_url)
        except ValueError as e:
            return ExecutionResult(
                success=False,
                data={"public_url": public_url},
                error=str(e),
            )

        # Find compose file in the Nextcloud stack directory
        stack_dir = Path(self.NEXTCLOUD_STACK_DIR)
        compose_file = self._find_compose_file(stack_dir)

        if compose_file is None:
            self.logger.warning("nextcloud_compose_not_found", dir=self.NEXTCLOUD_STACK_DIR)
            return ExecutionResult(
                success=False,
                data={"public_url": public_url, "host": host, "scheme": scheme},
                error=f"Nextcloud compose file not found in {self.NEXTCLOUD_STACK_DIR}. "
                      f"Looked for: {', '.join(self.COMPOSE_FILE_NAMES)}",
            )

        self.logger.info(
            "nextcloud_setting_domain",
            public_url=normalized_url,
            host=host,
            scheme=scheme,
            compose_file=str(compose_file),
        )

        start_time = time.time()
        logs: dict[str, str] = {}
        commands_executed = 0

        # Define the three occ commands to run
        occ_commands = [
            ("overwritehost", ["config:system:set", "overwritehost", f"--value={host}"]),
            ("overwriteprotocol", ["config:system:set", "overwriteprotocol", f"--value={scheme}"]),
            ("overwrite.cli.url", ["config:system:set", "overwrite.cli.url", f"--value={normalized_url}"]),
        ]

        try:
            for cmd_name, occ_args in occ_commands:
                exit_code, stdout, stderr = await self._run_occ_command(
                    compose_file,
                    occ_args,
                    timeout,
                )
                logs[cmd_name] = self._combine_output(stdout, stderr)
                commands_executed += 1

                if exit_code != 0:
                    duration_ms = (time.time() - start_time) * 1000
                    self.logger.warning(
                        "nextcloud_occ_command_failed",
                        command=cmd_name,
                        occ_args=occ_args,
                        exit_code=exit_code,
                        stderr=stderr[:500] if stderr else None,
                    )
                    return ExecutionResult(
                        success=False,
                        data={
                            "public_url": normalized_url,
                            "host": host,
                            "scheme": scheme,
                            "commands_executed": commands_executed,
                            "failed_command": cmd_name,
                            "failed_args": occ_args,
                            "logs": logs,
                        },
                        error=f"occ {cmd_name} failed with exit code {exit_code}",
                        duration_ms=duration_ms,
                    )

            duration_ms = (time.time() - start_time) * 1000
            self.logger.info(
                "nextcloud_domain_set",
                public_url=normalized_url,
                host=host,
                scheme=scheme,
                commands_executed=commands_executed,
                duration_ms=duration_ms,
            )

            return ExecutionResult(
                success=True,
                data={
                    "public_url": normalized_url,
                    "host": host,
                    "scheme": scheme,
                    "commands_executed": commands_executed,
                    "logs": logs,
                },
                duration_ms=duration_ms,
            )

        except asyncio.TimeoutError:
            duration_ms = (time.time() - start_time) * 1000
            self.logger.error(
                "nextcloud_timeout",
                public_url=normalized_url,
                timeout=timeout,
                commands_executed=commands_executed,
            )
            return ExecutionResult(
                success=False,
                data={
                    "public_url": normalized_url,
                    "host": host,
                    "scheme": scheme,
                    "commands_executed": commands_executed,
                    "logs": logs,
                },
                error=f"Nextcloud occ operation timed out after {timeout} seconds",
                duration_ms=duration_ms,
            )

        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            self.logger.error(
                "nextcloud_error",
                public_url=normalized_url,
                error=str(e),
                commands_executed=commands_executed,
            )
            return ExecutionResult(
                success=False,
                data={
                    "public_url": normalized_url,
                    "host": host,
                    "scheme": scheme,
                    "commands_executed": commands_executed,
                    "logs": logs,
                },
                error=str(e),
                duration_ms=duration_ms,
            )

    def _parse_public_url(self, public_url: str) -> tuple[str, str, str]:
        """Parse public URL into scheme, host, and normalized URL.

        Args:
            public_url: Full URL like "https://cloud.example.com" or just "cloud.example.com"

        Returns:
            Tuple of (scheme, host, normalized_url)
            - scheme: "http" or "https" (defaults to "https" if not provided)
            - host: Domain with optional port (e.g., "cloud.example.com:8443")
            - normalized_url: Full URL with trailing slash stripped

        Raises:
            ValueError: If URL is invalid or missing host
        """
        if not public_url or not public_url.strip():
            raise ValueError("public_url cannot be empty")

        url = public_url.strip()

        # Parse the URL
        parsed = urlparse(url)

        # Extract scheme, default to "https" if not provided
        scheme = parsed.scheme if parsed.scheme else "https"

        # Extract host (netloc includes port if present)
        host = parsed.netloc

        # Handle URLs without scheme (e.g., "cloud.example.com" or "cloud.example.com/path")
        # urlparse treats "cloud.example.com" as a path, not netloc
        if not host and not parsed.scheme:
            # The URL was provided without a scheme, so we need to re-parse with scheme
            url_with_scheme = f"https://{url}"
            parsed = urlparse(url_with_scheme)
            host = parsed.netloc
            scheme = "https"

        if not host:
            raise ValueError(f"Invalid URL - no host found: {public_url}")

        # Reconstruct normalized URL (with trailing slash stripped)
        normalized_url = f"{scheme}://{host}"
        if parsed.path and parsed.path != "/":
            normalized_url += parsed.path.rstrip("/")

        return scheme, host, normalized_url

    def _find_compose_file(self, compose_dir: Path) -> Path | None:
        """Find compose file in the directory.

        Searches in order: docker-compose.yml, compose.yml

        Args:
            compose_dir: Directory to search in

        Returns:
            Path to compose file, or None if not found
        """
        for filename in self.COMPOSE_FILE_NAMES:
            compose_file = compose_dir / filename
            if compose_file.exists():
                return compose_file
        return None

    def _combine_output(self, stdout: str, stderr: str) -> str:
        """Combine stdout and stderr into a single string.

        Args:
            stdout: Standard output
            stderr: Standard error

        Returns:
            Combined output string
        """
        parts = []
        if stdout:
            parts.append(stdout)
        if stderr:
            parts.append(stderr)
        return "\n".join(parts)

    async def _run_occ_command(
        self,
        compose_file: Path,
        occ_args: list[str],
        timeout: int,
    ) -> tuple[int, str, str]:
        """Run a Nextcloud occ command via docker compose exec.

        Args:
            compose_file: Path to the docker-compose file
            occ_args: Arguments to pass to occ (e.g., ["config:system:set", "overwritehost", "--value=..."])
            timeout: Operation timeout in seconds

        Returns:
            Tuple of (exit_code, stdout, stderr)
        """
        def _run() -> tuple[int, str, str]:
            # Build command: docker compose -f <file> exec -T --user <user> <service> php <occ_path> <args>
            cmd = [
                "docker",
                "compose",
                "-f",
                str(compose_file),
                "exec",
                "-T",  # Disable pseudo-TTY allocation
                "--user",
                self.NEXTCLOUD_USER,
                self.NEXTCLOUD_SERVICE_NAME,
                "php",
                self.NEXTCLOUD_OCC_PATH,
            ] + occ_args

            # Run command from stack directory, no shell=True
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=timeout,
                cwd=self.NEXTCLOUD_STACK_DIR,
            )

            return result.returncode, result.stdout, result.stderr

        return await asyncio.wait_for(
            asyncio.to_thread(_run),
            timeout=timeout + 30,  # Watchdog with buffer
        )
