"""Archive tool: create and extract ZIP and TAR archives."""

from __future__ import annotations

import json
import os
import tarfile
import zipfile
from pathlib import Path
from typing import Any

from loguru import logger

from madpod_agent.agent.tools.base import Tool

_MAX_EXTRACT_SIZE = 500 * 1024 * 1024  # 500 MB
_MAX_FILES_IN_ARCHIVE = 10_000


def _resolve_path(path: str, workspace: Path, allowed_dir: Path | None) -> Path:
    p = Path(path).expanduser()
    if not p.is_absolute():
        p = workspace / p
    resolved = p.resolve()
    if allowed_dir:
        try:
            resolved.relative_to(allowed_dir.resolve())
        except ValueError:
            raise PermissionError(f"Path {path} is outside allowed directory")
    return resolved


class ArchiveTool(Tool):
    """Create and extract ZIP and TAR archives."""

    def __init__(self, workspace: Path, allowed_dir: Path | None = None):
        self._workspace = workspace
        self._allowed_dir = allowed_dir

    @property
    def name(self) -> str:
        return "archive"

    @property
    def description(self) -> str:
        return (
            "Create or extract archive files (ZIP, TAR, TAR.GZ). "
            "Actions: 'create' (bundle files into archive), "
            "'extract' (unpack archive to directory), "
            "'list' (show archive contents without extracting)."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["create", "extract", "list"],
                    "description": "Operation to perform",
                },
                "archive_path": {
                    "type": "string",
                    "description": "Path to the archive file (relative to workspace)",
                },
                "files": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Files/directories to add (for 'create', relative to workspace)",
                },
                "extract_to": {
                    "type": "string",
                    "description": "Directory to extract into (for 'extract', default: archive name dir)",
                },
                "format": {
                    "type": "string",
                    "enum": ["zip", "tar", "tar.gz"],
                    "description": "Archive format (default: auto-detect from extension)",
                },
            },
            "required": ["action", "archive_path"],
        }

    async def execute(
        self,
        action: str,
        archive_path: str,
        files: list[str] | None = None,
        extract_to: str | None = None,
        format: str | None = None,
        **kwargs: Any,
    ) -> str:
        try:
            resolved_path = _resolve_path(archive_path, self._workspace, self._allowed_dir)
        except PermissionError as exc:
            return json.dumps({"error": str(exc)})

        fmt = format or self._detect_format(archive_path)

        if action == "create":
            return await self._create(resolved_path, files or [], fmt)
        elif action == "extract":
            return await self._extract(resolved_path, extract_to, fmt)
        elif action == "list":
            return await self._list(resolved_path, fmt)
        else:
            return json.dumps({"error": f"Unknown action: {action}"})

    def _detect_format(self, path: str) -> str:
        lower = path.lower()
        if lower.endswith(".tar.gz") or lower.endswith(".tgz"):
            return "tar.gz"
        elif lower.endswith(".tar"):
            return "tar"
        return "zip"

    async def _create(
        self, archive_path: Path, files: list[str], fmt: str,
    ) -> str:
        if not files:
            return json.dumps({"error": "No files specified to archive"})

        # Resolve all file paths
        resolved_files: list[tuple[Path, str]] = []
        for f in files:
            try:
                fp = _resolve_path(f, self._workspace, self._allowed_dir)
            except PermissionError as exc:
                return json.dumps({"error": str(exc)})
            if not fp.exists():
                return json.dumps({"error": f"File not found: {f}"})
            resolved_files.append((fp, f))

        archive_path.parent.mkdir(parents=True, exist_ok=True)

        try:
            if fmt == "zip":
                return self._create_zip(archive_path, resolved_files)
            else:
                return self._create_tar(archive_path, resolved_files, fmt)
        except Exception as exc:
            logger.error("Archive creation failed: {}", exc)
            return json.dumps({"error": f"Creation failed: {exc}"})

    def _create_zip(
        self, archive_path: Path, files: list[tuple[Path, str]],
    ) -> str:
        total_files = 0
        total_size = 0
        with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as zf:
            for fp, arcname in files:
                if fp.is_dir():
                    for child in fp.rglob("*"):
                        if child.is_file():
                            rel = str(child.relative_to(fp.parent))
                            zf.write(child, rel)
                            total_files += 1
                            total_size += child.stat().st_size
                else:
                    zf.write(fp, arcname)
                    total_files += 1
                    total_size += fp.stat().st_size

        archive_size = archive_path.stat().st_size
        logger.info("Created ZIP: {} ({} files, {:.1f} KB)", archive_path, total_files, archive_size / 1024)
        return json.dumps({
            "ok": True,
            "path": str(archive_path),
            "format": "zip",
            "files_count": total_files,
            "original_size": total_size,
            "archive_size": archive_size,
        })

    def _create_tar(
        self, archive_path: Path, files: list[tuple[Path, str]], fmt: str,
    ) -> str:
        mode = "w:gz" if fmt == "tar.gz" else "w"
        total_files = 0
        total_size = 0
        with tarfile.open(archive_path, mode) as tf:
            for fp, arcname in files:
                tf.add(fp, arcname=arcname)
                if fp.is_dir():
                    total_files += sum(1 for _ in fp.rglob("*") if _.is_file())
                    total_size += sum(c.stat().st_size for c in fp.rglob("*") if c.is_file())
                else:
                    total_files += 1
                    total_size += fp.stat().st_size

        archive_size = archive_path.stat().st_size
        logger.info("Created TAR: {} ({} files)", archive_path, total_files)
        return json.dumps({
            "ok": True,
            "path": str(archive_path),
            "format": fmt,
            "files_count": total_files,
            "original_size": total_size,
            "archive_size": archive_size,
        })

    async def _extract(
        self, archive_path: Path, extract_to: str | None, fmt: str,
    ) -> str:
        if not archive_path.exists():
            return json.dumps({"error": f"Archive not found: {archive_path.name}"})

        if extract_to:
            try:
                target_dir = _resolve_path(extract_to, self._workspace, self._allowed_dir)
            except PermissionError as exc:
                return json.dumps({"error": str(exc)})
        else:
            target_dir = archive_path.parent / archive_path.stem

        target_dir.mkdir(parents=True, exist_ok=True)

        try:
            if fmt == "zip":
                return self._extract_zip(archive_path, target_dir)
            else:
                return self._extract_tar(archive_path, target_dir)
        except Exception as exc:
            logger.error("Archive extraction failed: {}", exc)
            return json.dumps({"error": f"Extraction failed: {exc}"})

    def _extract_zip(self, archive_path: Path, target_dir: Path) -> str:
        with zipfile.ZipFile(archive_path, "r") as zf:
            # Security: check for path traversal
            for info in zf.infolist():
                member_path = (target_dir / info.filename).resolve()
                if not str(member_path).startswith(str(target_dir.resolve())):
                    return json.dumps({"error": f"Unsafe path in archive: {info.filename}"})
                if info.file_size > _MAX_EXTRACT_SIZE:
                    return json.dumps({"error": f"File too large: {info.filename}"})

            file_count = len([i for i in zf.infolist() if not i.is_dir()])
            if file_count > _MAX_FILES_IN_ARCHIVE:
                return json.dumps({"error": f"Too many files: {file_count} (max {_MAX_FILES_IN_ARCHIVE})"})

            zf.extractall(target_dir)

        logger.info("Extracted ZIP to {} ({} files)", target_dir, file_count)
        return json.dumps({
            "ok": True,
            "extracted_to": str(target_dir),
            "files_count": file_count,
        })

    def _extract_tar(self, archive_path: Path, target_dir: Path) -> str:
        with tarfile.open(archive_path, "r:*") as tf:
            # Security: check for path traversal
            members = tf.getmembers()
            for member in members:
                member_path = (target_dir / member.name).resolve()
                if not str(member_path).startswith(str(target_dir.resolve())):
                    return json.dumps({"error": f"Unsafe path in archive: {member.name}"})

            file_count = len([m for m in members if m.isfile()])
            if file_count > _MAX_FILES_IN_ARCHIVE:
                return json.dumps({"error": f"Too many files: {file_count} (max {_MAX_FILES_IN_ARCHIVE})"})

            tf.extractall(target_dir, filter="data")

        logger.info("Extracted TAR to {} ({} files)", target_dir, file_count)
        return json.dumps({
            "ok": True,
            "extracted_to": str(target_dir),
            "files_count": file_count,
        })

    async def _list(self, archive_path: Path, fmt: str) -> str:
        if not archive_path.exists():
            return json.dumps({"error": f"Archive not found: {archive_path.name}"})

        try:
            if fmt == "zip":
                return self._list_zip(archive_path)
            else:
                return self._list_tar(archive_path)
        except Exception as exc:
            return json.dumps({"error": f"Failed to list archive: {exc}"})

    def _list_zip(self, archive_path: Path) -> str:
        with zipfile.ZipFile(archive_path, "r") as zf:
            entries = []
            total_size = 0
            for info in zf.infolist():
                if not info.is_dir():
                    entries.append({
                        "name": info.filename,
                        "size": info.file_size,
                        "compressed_size": info.compress_size,
                    })
                    total_size += info.file_size

        return json.dumps({
            "ok": True,
            "format": "zip",
            "files_count": len(entries),
            "total_size": total_size,
            "entries": entries[:200],  # Cap listing
            "truncated": len(entries) > 200,
        })

    def _list_tar(self, archive_path: Path) -> str:
        with tarfile.open(archive_path, "r:*") as tf:
            entries = []
            total_size = 0
            for member in tf.getmembers():
                if member.isfile():
                    entries.append({
                        "name": member.name,
                        "size": member.size,
                    })
                    total_size += member.size

        return json.dumps({
            "ok": True,
            "format": "tar",
            "files_count": len(entries),
            "total_size": total_size,
            "entries": entries[:200],
            "truncated": len(entries) > 200,
        })
