"""Main client for KITECH Repository API."""

import asyncio
import hashlib
import logging
import tempfile
import zipfile
from pathlib import Path

import httpx
from tqdm import tqdm

from kitech_repository.core.auth import AuthManager
from kitech_repository.core.config import Config
from kitech_repository.core.exceptions import (
    ApiError,
    AuthenticationError,
    BatchDownloadError,
    DownloadError,
    UploadError,
)
from kitech_repository.models.file import (
    BatchDownloadResult,
    FailedDownload,
    File,
    FileDownloadInfo,
)
from kitech_repository.models.repository import Repository

logger = logging.getLogger(__name__)


class KitechClient:
    """Main client for interacting with KITECH Repository API."""

    def __init__(
        self,
        config: Config | None = None,
        app_key: str | None = None,
        api_version: str = "v1",
        token: str | None = None,  # Deprecated: use app_key instead
    ):
        """Initialize KITECH client.

        Args:
            config: Configuration object. If not provided, loads from Config.load()
            app_key: API app key. If provided, will login with this app key
            api_version: API version (default: "v1"). Use empty string for no version.
            token: (Deprecated) Use app_key instead. Kept for backward compatibility.
        """
        self.config = config or Config.load()
        self.api_version = api_version
        self.auth_manager = AuthManager(self.config)

        # Use provided app_key (or token for backward compatibility)
        key = app_key or token
        if key:
            self.auth_manager.login(app_key=key)

        # Construct base_url dynamically with version
        base_url = f"{self.config.server_url}/{api_version}" if api_version else self.config.server_url

        self.client = httpx.Client(
            base_url=base_url,
            timeout=httpx.Timeout(600.0, connect=30.0),  # 10 minutes total, 30s connect
            headers=(self.auth_manager.headers if self.auth_manager.is_authenticated() else {}),
        )

    def __enter__(self):
        """Context manager entry."""
        return self

    def __exit__(self, *args):
        """Context manager exit."""
        self._close()

    def _close(self):
        """Close the HTTP client."""
        self.client.close()

    def close(self):
        """Close the HTTP client (public API)."""
        self._close()

    def test_connection(self) -> dict:
        """Test connection and authentication to the API."""
        try:
            response = self.client.get("/cli/test")
            response.raise_for_status()
            return response.json()
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 401:
                raise AuthenticationError("Invalid or expired API token") from e
            raise ApiError(f"API request failed: {e}") from e
        except Exception as e:
            raise ApiError(f"Connection test failed: {e}") from e

    def list_repositories(
        self, page: int = 0, limit: int = 20, include_shared: bool = True
    ) -> dict[str, list[Repository]]:
        """List available repositories."""
        params = {
            "page": page,
            "limit": limit,
            "includeShared": include_shared,
        }

        try:
            response = self.client.get("/cli/repositories", params=params)
            response.raise_for_status()
            data = response.json()

            repositories = [Repository(**repo) for repo in data.get("repositories", [])]

            return {
                "repositories": repositories,
                "total_count": data.get("totalCount", 0),
            }
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 401:
                raise AuthenticationError("Invalid or expired API token") from e
            raise ApiError(f"Failed to list repositories: {e}") from e

    def list_files(
        self,
        repository_id: int,
        prefix: str = "",
        search: str | None = None,
        include_hash: bool = False,
        limit: int = 100,
        page: int = 0,
    ) -> dict[str, list[File]]:
        """List files in a repository."""
        params = {"limit": limit, "page": page}
        if prefix:
            params["prefix"] = prefix
        if search:
            params["search"] = search
        if include_hash:
            params["includeHash"] = include_hash

        try:
            # Try new API endpoint first: /cli/repositories/{id}/files
            response = self.client.get(f"/cli/repositories/{repository_id}/files", params=params)
            response.raise_for_status()
            data = response.json()

            # Handle different API response structures
            if "files" in data:
                # Real API structure: {"files": [...], "totalCount": ..., "repositoryId": ...}
                files = [File(**file_data) for file_data in data.get("files", [])]
                total_count = data.get("totalCount", len(files))
                has_more = False  # Real API doesn't provide pagination info
            elif "content" in data:
                # Mock API structure: {"content": [...], "meta": {...}}
                content = data.get("content", [])
                meta = data.get("meta", {})
                files = [File(**file_data) for file_data in content]
                total_count = meta.get("total", len(files))
                has_more = meta.get("page", 0) < meta.get("maxPage", 0) - 1 if "maxPage" in meta else False
            else:
                files = []
                total_count = 0
                has_more = False

            return {
                "files": files,
                "repository_id": repository_id,
                "prefix": prefix,
                "total_count": total_count,
                "has_more": has_more,
                "page": page,
                "limit": limit,
            }
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 401:
                raise AuthenticationError("Invalid or expired API token") from e
            elif e.response.status_code == 404:
                raise ApiError(f"Repository {repository_id} not found") from e
            raise ApiError(f"Failed to list files: {e}") from e

    def get_download_url(self, repository_id: int, path: str | None = None) -> str | dict:
        """Get download URL for a file or folder.

        Args:
            repository_id: Repository ID
            path: Path to file/folder. None = download entire repository (no path parameter sent)
                  Leading/trailing slashes are automatically removed.

        Returns:
            For single file: dict with url, sha256, md5, path
            For folder: dict with files array containing each file's download info
            For backward compatibility: may return URL string for old API
        """
        params = {}
        # IMPORTANT: When path is None (full repository download), we must NOT send
        # the path parameter at all - not even as "path=None" or "path="
        # The API expects the parameter to be completely omitted
        if path:
            # Normalize path: remove leading slashes only
            # Keep trailing slash for folder downloads (API requirement: folder paths must end with /)
            normalized_path = path.lstrip("/")
            if normalized_path:  # Only add if not empty after stripping
                params["path"] = normalized_path

        try:
            response = self.client.get(f"/cli/repositories/{repository_id}/download", params=params)
            response.raise_for_status()
            data = response.json()

            # Check if this is a folder download (new API structure)
            if data.get("success") and "files" in data and data.get("downloadUrl") is None:
                # Folder download: return full response with files array
                return data
            # Check if this is a single file download
            elif data.get("success") and "downloadUrl" in data and data["downloadUrl"]:
                # Single file: return simplified dict
                return {
                    "url": data["downloadUrl"],
                    "sha256": data.get("sha256"),
                    "md5": data.get("md5"),
                    "path": data.get("path"),
                }
            else:
                # Fallback for old response format - return just URL
                return data.get("downloadUrl")
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 401:
                raise AuthenticationError("Invalid or expired API token") from e
            elif e.response.status_code == 404:
                raise ApiError("Repository or path not found") from e
            raise ApiError(f"Failed to get download URL: {e}") from e

    def _stream_download(
        self,
        client: httpx.Client,
        url: str,
        output_path: Path,
        show_progress: bool,
        description: str,
    ) -> None:
        """Helper to stream download with progress tracking."""
        with client.stream("GET", url) as response:
            response.raise_for_status()
            total_size = int(response.headers.get("Content-Length", 0))

            with open(output_path, "wb") as f:
                if show_progress and total_size > 0:
                    with tqdm(total=total_size, unit="B", unit_scale=True, desc=description) as pbar:
                        for chunk in response.iter_bytes(chunk_size=self.config.chunk_size):
                            if chunk:
                                f.write(chunk)
                                pbar.update(len(chunk))
                        f.flush()
                else:
                    for chunk in response.iter_bytes(chunk_size=self.config.chunk_size):
                        if chunk:
                            f.write(chunk)
                    f.flush()

    def download_folder(
        self,
        repository_id: int,
        remote_path: str | None = None,
        local_path: Path | str | None = None,
        show_progress: bool = True,
    ) -> Path:
        """Download a folder from repository.

        Args:
            repository_id: Repository ID to download from
            remote_path: Path to folder in repository (None for full repository)
            local_path: Local directory to save folder (None for default download directory)
            show_progress: Show download progress

        Returns:
            Path to downloaded folder
        """
        download_info = self.get_download_url(repository_id, remote_path)

        # Convert string to Path if necessary
        if local_path is not None and not isinstance(local_path, Path):
            local_path = Path(local_path)

        local_path = local_path or self.config.download_dir
        local_path.mkdir(parents=True, exist_ok=True)

        # Check if this is a folder download (API structure with files array)
        if isinstance(download_info, dict) and "files" in download_info:
            # Folder download: download each file individually
            if download_info.get("path"):
                # Specific folder download - create folder in local_path
                folder_name = Path(download_info["path"]).name.rstrip("/")
                folder_path = local_path / folder_name
                folder_path.mkdir(parents=True, exist_ok=True)
            else:
                # Full repository download - extract directly to local_path
                folder_path = local_path

            files = download_info["files"]

            # Filter out directories - only download actual files
            actual_files = [f for f in files if not f.get("isDir") and not f.get("isDirectory")]

            logger.debug(f"[DOWNLOAD] Total items: {len(files)}, Actual files: {len(actual_files)}")
            logger.debug(f"[DOWNLOAD] download_info.get('path'): {download_info.get('path')!r}")

            # Prepare download tasks
            download_tasks = []
            for file_info in actual_files:
                # Get the full path - try logicalPath, then objectName, then path, then name as fallback
                logical_path = file_info.get("logicalPath")
                object_name = file_info.get("objectName")
                path = file_info.get("path")
                name = file_info.get("name")

                file_path_str = logical_path or object_name or path or name
                file_name = name or "unknown"
                download_url = file_info["downloadUrl"]

                # Extract relative path
                # For full repository download (path=null): use full path as-is
                # For specific folder download (path="folder/"): remove folder prefix
                if download_info.get("path"):
                    # Specific folder download - remove folder prefix
                    folder_prefix = download_info["path"]
                    if file_path_str.startswith(folder_prefix):
                        relative_path = file_path_str[len(folder_prefix) :]
                    else:
                        # Fallback to filename if path doesn't start with folder prefix
                        relative_path = file_name
                else:
                    # Full repository download - preserve full folder structure
                    relative_path = file_path_str

                logger.debug(f"[DOWNLOAD] Relative path for download: {relative_path!r}")

                # Create full local path with folder structure
                local_file_path = folder_path / relative_path
                logger.debug(f"[DOWNLOAD] Local file path: {local_file_path}")

                download_tasks.append((download_url, local_file_path, file_name))

            # Download all files concurrently
            asyncio.run(self._download_files_concurrent(download_tasks, show_progress))

            return folder_path
        else:
            raise DownloadError(
                "Expected folder download but got file. Use download_file() for single file downloads."
            )

    def download_file(
        self,
        repository_id: int,
        remote_path: str,
        local_path: Path | str | None = None,
        show_progress: bool = True,
    ) -> Path:
        """Download a single file from repository.

        Args:
            repository_id: Repository ID to download from
            remote_path: Path to file in repository
            local_path: Local directory or file path to save (None for default download directory)
            show_progress: Show download progress

        Returns:
            Path to downloaded file
        """
        download_info = self.get_download_url(repository_id, remote_path)

        # Convert string to Path if necessary
        if local_path is not None and not isinstance(local_path, Path):
            local_path = Path(local_path)

        # Check if this is actually a folder download
        if isinstance(download_info, dict) and "files" in download_info:
            raise DownloadError(
                "Expected file download but got folder. Use download_folder() for folder downloads."
            )

        # Handle single file download (dict or string)
        if isinstance(download_info, dict):
            download_url = download_info.get("url")
            file_hash = download_info.get("sha256")
        else:
            download_url = download_info
            file_hash = None

        # Determine output path
        if local_path is None:
            # Use default download directory
            output_dir = self.config.download_dir
            output_dir.mkdir(parents=True, exist_ok=True)
            filename = Path(remote_path).name
            download_path = output_dir / filename
        elif local_path.is_dir() or not local_path.suffix:
            # local_path is a directory
            local_path.mkdir(parents=True, exist_ok=True)
            filename = Path(remote_path).name
            download_path = local_path / filename
        else:
            # local_path is a file path
            local_path.parent.mkdir(parents=True, exist_ok=True)
            download_path = local_path

        try:
            # Determine description for progress bar
            desc = Path(remote_path).name if remote_path else "Download"

            # Use appropriate client based on URL type
            if "X-Amz-Algorithm" in download_url:
                # Clean client for presigned URLs
                with httpx.Client(timeout=httpx.Timeout(600.0, connect=30.0)) as clean_client:
                    self._stream_download(clean_client, download_url, download_path, show_progress, desc)
            else:
                # Authenticated client for regular API URLs
                self._stream_download(self.client, download_url, download_path, show_progress, desc)

            return download_path

        except Exception as e:
            raise DownloadError(f"Failed to download file: {e}") from e

    def get_batch_download_info(self, repository_id: int, paths: list[str]) -> list[FileDownloadInfo]:
        """Get download information for multiple files."""
        try:
            response = self.client.post(
                f"/cli/repositories/{repository_id}/download-list",
                json={"paths": paths},
            )
            response.raise_for_status()
            data = response.json()

            return [FileDownloadInfo(**item) for item in data.get("items", [])]
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 401:
                raise AuthenticationError("Invalid or expired API token") from e
            raise ApiError(f"Failed to get batch download info: {e}") from e

    async def download_batch(
        self,
        repository_id: int,
        paths: list[str],
        output_dir: Path | None = None,
        raise_on_failure: bool = True,
    ) -> list[Path] | BatchDownloadResult:
        """Download multiple files in batch.

        Args:
            repository_id: Repository ID to download from
            paths: List of file paths to download
            output_dir: Directory to save downloaded files (uses config.download_dir if not specified)
            raise_on_failure: If True (default), raise BatchDownloadError on any failure.
                            If False, return BatchDownloadResult with partial success.

        Returns:
            - If raise_on_failure=True (default): list[Path] on complete success
            - If raise_on_failure=False: BatchDownloadResult with success/failure details

        Raises:
            BatchDownloadError: When raise_on_failure=True and any downloads fail.
                                Contains both successful and failed download information.

        Examples:
            Basic usage (all succeed or exception):
            >>> try:
            ...     files = await client.download_batch(123, ["/data/file1.csv"])
            ...     for file in files:  # list[Path]
            ...         process(file)
            ... except BatchDownloadError as e:
            ...     print(f"Failed: {e.failure_count}/{e.total_count}")
            ...     for failure in e.failed:
            ...         print(f"  {failure.name}: {failure.error}")
            ...     # Still process successful downloads
            ...     for file in e.successful:
            ...         process(file)

            Partial failure allowed:
            >>> result = await client.download_batch(123, paths, raise_on_failure=False)
            >>> if result.has_failures:
            ...     for failure in result.failed:
            ...         retry(failure)
        """
        download_infos = self.get_batch_download_info(repository_id, paths)
        output_dir = output_dir or self.config.download_dir
        output_dir.mkdir(parents=True, exist_ok=True)

        async with httpx.AsyncClient() as client:
            tasks = []
            file_infos = []
            for info in download_infos:
                if info.type == "file":
                    task = self._async_download_file(
                        client,
                        info.presigned_url,
                        output_dir / info.name,
                    )
                    tasks.append(task)
                    file_infos.append(info)

            results = await asyncio.gather(*tasks, return_exceptions=True)

        # Separate successful and failed downloads
        successful_paths: list[Path] = []
        failed_downloads: list[FailedDownload] = []

        for i, result in enumerate(results):
            if isinstance(result, Path):
                successful_paths.append(result)
            else:
                # result is an exception
                error_msg = str(result)
                failed_downloads.append(
                    FailedDownload(
                        path=file_infos[i].path,
                        name=file_infos[i].name,
                        error=error_msg,
                    )
                )

        # Handle failures based on raise_on_failure parameter
        if failed_downloads:
            if raise_on_failure:
                raise BatchDownloadError(successful_paths, failed_downloads)
            else:
                return BatchDownloadResult(successful=successful_paths, failed=failed_downloads)

        # All successful - return simple list
        return successful_paths

    async def _async_download_file(
        self,
        client: httpx.AsyncClient,
        url: str,
        output_path: Path,
    ) -> Path:
        """Async helper for downloading a single file."""
        try:
            response = await client.get(url)
            response.raise_for_status()
            output_path.write_bytes(response.content)
            return output_path
        except Exception as e:
            raise DownloadError(f"Failed to download {output_path.name}: {e}") from e

    async def _download_files_concurrent(
        self,
        download_tasks: list[tuple[str, Path, str]],
        show_progress: bool = True,
    ) -> None:
        """Download multiple files concurrently.

        Args:
            download_tasks: List of (download_url, local_file_path, file_name) tuples
            show_progress: Whether to show progress bar
        """
        async with httpx.AsyncClient(timeout=httpx.Timeout(600.0, connect=30.0)) as client:
            tasks = []

            # Create parent directories for all files
            for _, local_file_path, _ in download_tasks:
                local_file_path.parent.mkdir(parents=True, exist_ok=True)

            # Create async tasks
            for download_url, local_file_path, file_name in download_tasks:
                task = self._async_download_file(client, download_url, local_file_path)
                tasks.append(task)

            # Download concurrently with optional progress bar
            if show_progress:
                from tqdm import tqdm

                # Use tqdm to track completed downloads
                with tqdm(total=len(tasks), desc="Downloading files", unit="file") as pbar:
                    for coro in asyncio.as_completed(tasks):
                        try:
                            await coro
                            pbar.update(1)
                        except Exception as e:
                            logger.error(f"Download failed: {e}")
                            pbar.update(1)
            else:
                # Just gather all results without progress
                await asyncio.gather(*tasks, return_exceptions=True)

    def upload_file(
        self,
        repository_id: int,
        file_path: Path,
        remote_path: str = "",
        show_progress: bool = True,
        _skip_permission_check: bool = False,
    ) -> dict:
        """Upload a file to dataset (repository).

        Note: httpx uses standard multipart form data upload which is efficient
        and memory-safe for files of all sizes. Progress tracking is limited to
        start/end notifications due to httpx's architecture.

        Args:
            repository_id: Repository ID
            file_path: Local file path
            remote_path: Remote path within repository
            show_progress: Show progress messages
            _skip_permission_check: Internal parameter to skip permission check (for bulk uploads)

        Raises:
            UploadError: If user has VIEWER role (no upload permission)
        """
        # Check upload permission (VIEWER role cannot upload)
        if not _skip_permission_check:
            repos_result = self.list_repositories(page=0, limit=100)
            repository = next((r for r in repos_result["repositories"] if r.id == repository_id), None)

            if repository is None:
                raise UploadError(f"Repository {repository_id} not found")

            if repository.user_role == "VIEWER":
                raise UploadError(
                    "Upload permission denied: You have VIEWER role. Only OWNER and ADMIN roles can upload files."
                )

        # Convert to Path if string
        if isinstance(file_path, str):
            file_path = Path(file_path)

        if not file_path.exists():
            raise UploadError(f"File not found: {file_path}")

        # Normalize remote_path: remove leading slashes only
        # Keep trailing slash for folder paths (API requirement: folder paths should end with /)
        remote_path = remote_path.lstrip("/")

        # Remove filename from remote_path if present
        # API automatically appends the filename, so we only send the directory path
        if remote_path:
            from pathlib import Path as PathLib

            # If remote_path ends with a filename (has extension), remove it
            path_parts = PathLib(remote_path)
            if path_parts.suffix:  # Has extension like .txt, .csv
                # Remove the filename part, keep only directory
                remote_path = str(path_parts.parent) if path_parts.parent != PathLib(".") else ""
                remote_path = remote_path.replace("\\", "/").lstrip("/")
                # Add trailing slash so API knows it's a directory
                if remote_path:
                    remote_path = remote_path + "/"

        try:
            file_size = file_path.stat().st_size

            if show_progress:
                logger.info(f"Uploading {file_path.name} ({file_size / 1024 / 1024:.2f} MB)...")

            # Calculate MD5 hash for integrity verification
            md5_hash = hashlib.md5()
            with open(file_path, "rb") as f:
                for chunk in iter(lambda: f.read(8192), b""):
                    md5_hash.update(chunk)
            md5_digest = md5_hash.hexdigest()

            if show_progress:
                logger.info(f"MD5: {md5_digest}")

            # Upload file with MD5 hash
            with open(file_path, "rb") as f:
                files = {"file": (file_path.name, f)}
                data = {"path": remote_path, "md5": md5_digest}

                response = self.client.post(f"/cli/repositories/{repository_id}/upload", files=files, data=data)

            response.raise_for_status()

            if show_progress:
                logger.info(f"✓ Upload completed: {file_path.name}")

            return response.json()
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 401:
                raise AuthenticationError("Invalid or expired API token") from e
            raise UploadError(f"Failed to upload file: {e}") from e
