"""Studio Web service to interact with files and projects."""

import json
import os
from pathlib import Path

import httpx

from uipath.robot.infra import get_logger
from uipath.robot.models import StudioWebFile, StudioWebProject


class StudioWebService:
    """Service for interacting with Studio Web APIs."""

    def __init__(self, project_id: str):
        """Initialize the StudioWebService with environment variables."""
        self.logger = get_logger()

        self.access_token = os.getenv("UIPATH_ACCESS_TOKEN")
        self.base_url = os.getenv("UIPATH_URL")
        self.project_id = project_id
        self.base_path = Path.cwd() / ".uipath"
        self.studio_metadata_file = "studio_metadata.json"
        self.agent_json_file = "agent.json"

        if not self.access_token:
            raise ValueError("Missing UIPATH_ACCESS_TOKEN environment variable")
        if not self.base_url:
            raise ValueError("Missing UIPATH_URL environment variable")

    async def download_studio_web_project_async(self) -> Path | None:
        """Download Studio Web Project based on the ProjectId environment variable.

        Returns:
            Path to the downloaded project directory, or None if failed
        """
        # Check if project already exists locally
        project_dir = self.base_path / "projects" / self.project_id

        studio_project = await self._get_project_structure_async()

        if not studio_project:
            self.logger.error("Studio Web Project does not exist")
            return None

        if project_dir.exists():
            self.logger.info(f"Project already downloaded at {project_dir}")

            should_download = await self._should_download_project(
                studio_project, project_dir
            )
            if not should_download:
                return project_dir

        # Build flat dictionary of files with their paths
        files_dict = self._build_file_dictionary(studio_project)

        result = await self._setup_studio_web_project_async(files_dict)
        return result

    async def _get_project_structure_async(self) -> StudioWebProject | None:
        """Get the project structure from Studio Web.

        Returns:
            StudioWebProject object if successful, None otherwise
        """
        assert self.base_url is not None
        assert self.project_id is not None

        base_url = self.base_url.rstrip("/").rsplit("/", 1)[0]
        url = f"{base_url}/studio_/backend/api/project/{self.project_id}/fileoperations/structure"

        headers = {
            "Authorization": f"Bearer {self.access_token}",
            "Content-Type": "application/json",
        }

        async with httpx.AsyncClient() as client:
            try:
                response = await client.get(url, headers=headers)
                response.raise_for_status()

                data = response.json()

                project = StudioWebProject.model_validate(data)
                return project

            except Exception as e:
                self.logger.error(f"Failed to get project structure due to error: {e}")
                return None

    async def _should_download_project(
        self, studio_project: StudioWebProject, project_dir: Path
    ) -> bool:
        """Check if the project needs to be re-downloaded by comparing timestamps.

        Args:
            studio_project: The project structure from Studio Web
            project_dir: The local project directory

        Returns:
            True if project should be re-downloaded, False if cached version is up-to-date
        """
        uipath_folder = None
        if studio_project.folders:
            for folder in studio_project.folders:
                if folder.name == ".uipath":
                    uipath_folder = folder
                    break

        metadata_locations = []

        # First check studio_metadata.json
        if uipath_folder and uipath_folder.files:
            metadata_locations.append(
                (
                    uipath_folder.files,
                    "studio_metadata.json",
                    project_dir / ".uipath" / "studio_metadata.json",
                    lambda json_data: json_data.get("lastPushDate"),
                )
            )

        # Fallback to agent.json
        if studio_project.files:
            metadata_locations.append(
                (
                    studio_project.files,
                    "agent.json",
                    project_dir / "agent.json",
                    lambda json_data: json_data.get("metadata", {}).get("pushDate"),
                )
            )

        # Try each metadata location
        for file_list, file_name, local_path, timestamp_extractor in metadata_locations:
            file_id = self._find_file_by_name(file_list, file_name)
            if file_id:
                return await self._compare_timestamps(
                    file_id, local_path, timestamp_extractor
                )

        # If we reach here, no metadata file was found
        self.logger.warning(
            "Metadata file not found in remote project, using cached version"
        )
        return False

    def _find_file_by_name(
        self, files: list[StudioWebFile], file_name: str
    ) -> str | None:
        """Find a file by name in a list of files.

        Args:
            files: List of StudioWebFile objects
            file_name: Name of the file to find

        Returns:
            File ID if found, None otherwise
        """
        for file in files:
            if file.name == file_name:
                return file.id
        return None

    async def _compare_timestamps(
        self, file_id: str, local_path: Path, timestamp_extractor
    ) -> bool:
        """Compare remote and local timestamps to determine if re-download is needed.

        Args:
            file_id: The remote file ID to download
            local_path: Path to the local metadata file
            timestamp_extractor: Function to extract timestamp from JSON data

        Returns:
            True if project should be re-downloaded, False if up-to-date
        """
        # Download and parse the metadata file from Studio Web
        metadata_content = await self._download_project_file_async(file_id)
        if not metadata_content:
            self.logger.warning(
                "Failed to download metadata file, using cached version"
            )
            return False

        remote_metadata = json.loads(metadata_content.decode("utf-8"))
        remote_timestamp = timestamp_extractor(remote_metadata)

        # Read local metadata file
        if not local_path.exists():
            self.logger.warning("Local metadata file not found, re-downloading project")
            return True

        local_metadata = json.loads(local_path.read_text())
        local_timestamp = timestamp_extractor(local_metadata)

        # Compare timestamps
        if remote_timestamp and local_timestamp:
            if remote_timestamp > local_timestamp:
                self.logger.info(
                    f"Remote project is newer (remote: {remote_timestamp}, local: {local_timestamp}). Downloading remote version."
                )
                return True
            else:
                self.logger.info(
                    f"Local project is up-to-date (remote: {remote_timestamp}, local: {local_timestamp})"
                )
                return False
        else:
            self.logger.warning(
                "Could not compare timestamps, downloading the remote version"
            )
            return True

    def _build_file_dictionary(
        self, node: StudioWebProject, prefix: str = ""
    ) -> dict[str, StudioWebFile]:
        """Recursively build a dictionary of files with their paths.

        Args:
            node: The project/folder node to process
            prefix: The current path prefix

        Returns:
            Dictionary mapping file paths to StudioWebFile objects
        """
        result: dict[str, StudioWebFile] = {}

        # Add files from current node
        if node.files:
            for file in node.files:
                key = file.name if not prefix else f"{prefix}/{file.name}"
                if key not in result:
                    result[key] = file

        # Recursively process subfolders
        if node.folders:
            for folder in node.folders:
                if folder.name:
                    next_prefix = (
                        folder.name if not prefix else f"{prefix}/{folder.name}"
                    )
                    subfolder_files = self._build_file_dictionary(folder, next_prefix)
                    result.update(subfolder_files)

        return result

    async def _setup_studio_web_project_async(
        self, files_dict: dict[str, StudioWebFile]
    ) -> Path | None:
        """Setup the Studio Web project by downloading all files.

        Args:
            files_dict: Dictionary mapping file paths to StudioWebFile objects

        Returns:
            Path to the project directory, or None if failed
        """
        projects_base_directory = self.base_path / "projects"

        if len(files_dict) == 0:
            self.logger.warning("No files to download")
            return None

        # Create directory for the project
        project_dir = projects_base_directory / self.project_id
        project_dir.mkdir(parents=True, exist_ok=True)

        for file_path, file in files_dict.items():
            file_content = await self._download_project_file_async(file.id)

            if file_content is None:
                self.logger.error(f"Failed to download file: {file_path}")
                return None

            destination_path = project_dir / file_path
            destination_directory = destination_path.parent
            destination_directory.mkdir(parents=True, exist_ok=True)
            destination_path.write_bytes(file_content)

        self.logger.info(
            f"Successfully downloaded {len(files_dict)} files to {project_dir}"
        )
        return project_dir

    async def _download_project_file_async(self, file_id: str) -> bytes | None:
        """Download a file from Studio Web by its ID.

        Args:
            file_id: The ID of the file to download

        Returns:
            File content as bytes if successful, None otherwise
        """
        assert self.base_url is not None
        assert self.project_id is not None

        base_url = self.base_url.rstrip("/").rsplit("/", 1)[0]
        url = f"{base_url}/studio_/backend/api/project/{self.project_id}/fileoperations/file/{file_id}"

        headers = {
            "Authorization": f"Bearer {self.access_token}",
        }

        async with httpx.AsyncClient() as client:
            try:
                response = await client.get(url, headers=headers)
                response.raise_for_status()
                return response.content

            except Exception as e:
                self.logger.error(
                    f"Failed to download file with Id {file_id} due to error: {e}"
                )
                return None
