"""BitableTool — Agent tool for bitable data ingestion and CRUD via HTTP.

Implements KTD5 (REST API boundary even when co-deployed) and KTD11
(internal service token auth). The tool uses ``httpx.AsyncClient`` to call
the bitable REST API; it never imports BitableService directly.

Actions: create_table, import_excel, import_database, collect_api,
         upsert_records, query_records.

Batch chunking: upsert and import operations send at most ``BATCH_SIZE``
records per HTTP request. On partial failure, the result includes
``successful_count`` and ``resume_from`` for breakpoint continuation.
"""

from __future__ import annotations

import asyncio
import logging
from typing import Any

import httpx

from agentkit.bitable.ingestion.excel import ParsedSheet, parse_excel, parse_excel_url
from agentkit.bitable.ingestion.database import import_table as import_db_table
from agentkit.bitable.ingestion.api_collector import transform_records
from agentkit.tools.base import Tool

logger = logging.getLogger(__name__)

BATCH_SIZE = 500


class BitableTool(Tool):
    """Agent tool for bitable operations via REST API.

    Args:
        base_url: Bitable API base URL (e.g. ``http://localhost:8001/api/v1/bitable``).
        internal_token: Service token for KTD11 auth. If ``None``, requests
            go unauthenticated (will fail if the server requires auth).
    """

    def __init__(self, base_url: str, internal_token: str | None = None) -> None:
        super().__init__(
            name="bitable",
            description=(
                "Create and manage bitable (multi-dimensional spreadsheet) tables, "
                "ingest data from Excel files, databases, or API responses, and "
                "query records. Actions: create_table, import_excel, "
                "import_database, collect_api, upsert_records, query_records."
            ),
            input_schema={
                "type": "object",
                "properties": {
                    "action": {
                        "type": "string",
                        "enum": [
                            "create_table",
                            "import_excel",
                            "import_database",
                            "collect_api",
                            "upsert_records",
                            "query_records",
                        ],
                        "description": "Bitable operation to perform.",
                    },
                    "table_name": {
                        "type": "string",
                        "description": "Name for the new bitable table (create_table, import_excel, import_database).",
                    },
                    "description": {
                        "type": "string",
                        "description": "Table description (create_table).",
                    },
                    "file_path": {
                        "type": "string",
                        "description": "Path to .xlsx file (import_excel).",
                    },
                    "file_url": {
                        "type": "string",
                        "description": "URL to download .xlsx file (import_excel).",
                    },
                    "connection_string": {
                        "type": "string",
                        "description": "Database connection string (import_database).",
                    },
                    "table_names": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Source table names to import (import_database).",
                    },
                    "table_id": {
                        "type": "string",
                        "description": "Target bitable table ID (collect_api, upsert_records, query_records).",
                    },
                    "records": {
                        "type": "array",
                        "description": "Records to write (collect_api, upsert_records).",
                    },
                    "field_mapping": {
                        "type": "object",
                        "description": "Mapping {source_key: bitable_field_id} (collect_api).",
                    },
                    "primary_key_field_id": {
                        "type": "string",
                        "description": "Field ID of the primary key (upsert_records, collect_api).",
                    },
                    "resume_from": {
                        "type": "integer",
                        "description": "Skip this many records before resuming a failed batch (upsert_records, collect_api).",
                    },
                    "cursor": {
                        "type": "string",
                        "description": "Pagination cursor (query_records).",
                    },
                    "limit": {
                        "type": "integer",
                        "description": "Max records to return (query_records).",
                    },
                },
                "required": ["action"],
            },
        )
        self._base_url = base_url.rstrip("/")
        self._internal_token = internal_token
        self._client: httpx.AsyncClient | None = None

    async def _get_client(self) -> httpx.AsyncClient:
        if self._client is None or self._client.is_closed:
            headers: dict[str, str] = {}
            if self._internal_token:
                headers["X-Internal-Token"] = self._internal_token
            self._client = httpx.AsyncClient(
                base_url=self._base_url,
                headers=headers,
                timeout=60.0,
            )
        return self._client

    async def close(self) -> None:
        if self._client is not None and not self._client.is_closed:
            await self._client.aclose()

    async def execute(self, **kwargs) -> dict[str, Any]:
        action = kwargs.get("action")
        handlers = {
            "create_table": self._create_table,
            "import_excel": self._import_excel,
            "import_database": self._import_database,
            "collect_api": self._collect_api,
            "upsert_records": self._upsert_records,
            "query_records": self._query_records,
        }
        handler = handlers.get(action)
        if handler is None:
            return {"success": False, "error": f"Unknown action: {action!r}"}
        try:
            return await handler(**kwargs)
        except httpx.HTTPStatusError as e:
            return {
                "success": False,
                "error": f"Bitable API error {e.response.status_code}: {e.response.text[:500]}",
            }
        except httpx.ConnectError as e:
            return {"success": False, "error": f"Cannot connect to bitable API: {e}"}
        except Exception as e:
            return {"success": False, "error": f"{action} failed: {e}"}

    # ------------------------------------------------------------------
    # create_table
    # ------------------------------------------------------------------

    async def _create_table(self, **kwargs) -> dict[str, Any]:
        table_name = kwargs.get("table_name")
        if not table_name:
            return {"success": False, "error": "Missing required field: table_name"}
        client = await self._get_client()
        resp = await client.post(
            "/tables",
            json={"name": table_name, "description": kwargs.get("description", "")},
        )
        resp.raise_for_status()
        data = resp.json()
        return {"success": True, "table": data["table"]}

    # ------------------------------------------------------------------
    # import_excel
    # ------------------------------------------------------------------

    async def _import_excel(self, **kwargs) -> dict[str, Any]:
        file_path = kwargs.get("file_path")
        file_url = kwargs.get("file_url")
        if not file_path and not file_url:
            return {"success": False, "error": "Either file_path or file_url is required"}

        # Parse Excel — offload sync I/O to thread pool (P2 #21-23).
        if file_path:
            sheets = await asyncio.to_thread(parse_excel, file_path)
        else:
            sheets = await asyncio.to_thread(parse_excel_url, file_url)

        if not sheets:
            return {"success": False, "error": "Excel file has no sheets with data"}

        results: list[dict[str, Any]] = []
        for sheet in sheets:
            result = await self._import_sheet(sheet)
            results.append(result)
        return {"success": True, "sheets": results}

    async def _import_sheet(self, sheet: ParsedSheet) -> dict[str, Any]:
        """Create a bitable table from a parsed sheet and upsert all rows."""
        client = await self._get_client()

        # 1. Create table
        resp = await client.post("/tables", json={"name": sheet.name})
        resp.raise_for_status()
        table_id = resp.json()["table"]["id"]

        # 2. Create fields
        field_name_to_id: dict[str, str] = {}
        for col_name, field_type in zip(sheet.columns, sheet.field_types):
            resp = await client.post(
                f"/tables/{table_id}/fields",
                json={"name": col_name, "field_type": field_type, "owner": "agent"},
            )
            resp.raise_for_status()
            field_id = resp.json()["field"]["id"]
            field_name_to_id[col_name] = field_id

        # 3. Map record keys to field IDs and batch upsert
        mapped_records = [
            {field_name_to_id[k]: v for k, v in rec.items() if k in field_name_to_id}
            for rec in sheet.records
        ]

        if not mapped_records:
            return {
                "table_id": table_id,
                "table_name": sheet.name,
                "field_count": len(field_name_to_id),
                "record_count": 0,
            }

        # Use first field as PK fallback (import_excel doesn't require a PK)
        # If no PK is set, upsert won't work — use create_records instead
        upsert_result = await self._batch_create_records(table_id, mapped_records)
        return {
            "table_id": table_id,
            "table_name": sheet.name,
            "field_count": len(field_name_to_id),
            "record_count": upsert_result["successful_count"],
            **upsert_result,
        }

    async def _batch_create_records(
        self, table_id: str, records: list[dict[str, Any]]
    ) -> dict[str, Any]:
        """Create records in batches via POST /tables/{id}/records."""
        client = await self._get_client()
        total = len(records)
        successful = 0
        errors: list[dict[str, Any]] = []

        for start in range(0, total, BATCH_SIZE):
            batch = records[start : start + BATCH_SIZE]
            try:
                resp = await client.post(
                    f"/tables/{table_id}/records",
                    json={"records": batch},
                )
                resp.raise_for_status()
                successful += len(batch)
            except httpx.HTTPStatusError as e:
                errors.append(
                    {
                        "batch_start": start,
                        "batch_size": len(batch),
                        "status": e.response.status_code,
                        "error": e.response.text[:300],
                    }
                )
                break  # stop on first failure

        return {
            "successful_count": successful,
            "total": total,
            "resume_from": successful,
            **({"errors": errors} if errors else {}),
        }

    # ------------------------------------------------------------------
    # import_database
    # ------------------------------------------------------------------

    async def _import_database(self, **kwargs) -> dict[str, Any]:
        conn_str = kwargs.get("connection_string")
        table_names = kwargs.get("table_names")
        if not conn_str:
            return {"success": False, "error": "Missing required field: connection_string"}
        if not table_names:
            return {"success": False, "error": "Missing required field: table_names"}

        results: list[dict[str, Any]] = []
        for src_table in table_names:
            try:
                # Offload sync DB reflection to thread pool (P2 #21-23).
                reflected = await asyncio.to_thread(import_db_table, conn_str, src_table)
                result = await self._import_reflected_table(reflected)
                results.append(result)
            except ConnectionError as e:
                return {"success": False, "error": str(e), "imported": results}
            except Exception as e:
                results.append({"table_name": src_table, "success": False, "error": str(e)})
        return {"success": True, "tables": results}

    async def _import_reflected_table(self, reflected: dict[str, Any]) -> dict[str, Any]:
        """Create a bitable table from reflected DB data and upsert rows."""
        client = await self._get_client()
        table_name = reflected["table_name"]

        # 1. Create table
        resp = await client.post("/tables", json={"name": table_name})
        resp.raise_for_status()
        table_id = resp.json()["table"]["id"]

        # 2. Create fields
        field_name_to_id: dict[str, str] = {}
        pk_field_id: str | None = None
        for fdef in reflected["fields"]:
            resp = await client.post(
                f"/tables/{table_id}/fields",
                json={
                    "name": fdef["name"],
                    "field_type": fdef["field_type"],
                    "owner": "agent",
                },
            )
            resp.raise_for_status()
            fid = resp.json()["field"]["id"]
            field_name_to_id[fdef["name"]] = fid
            if fdef.get("is_primary_key"):
                pk_field_id = fid

        # 3. Set primary key
        if pk_field_id:
            await client.patch("/tables/" + table_id, json={"primary_key_field_id": pk_field_id})

        # 4. Map and upsert records
        mapped = [
            {field_name_to_id[k]: v for k, v in rec.items() if k in field_name_to_id}
            for rec in reflected["records"]
        ]

        if not mapped:
            return {
                "table_id": table_id,
                "table_name": table_name,
                "record_count": 0,
                "success": True,
            }

        if pk_field_id:
            upsert = await self._batch_upsert(table_id, mapped, pk_field_id)
        else:
            upsert = await self._batch_create_records(table_id, mapped)

        return {
            "table_id": table_id,
            "table_name": table_name,
            "record_count": upsert["successful_count"],
            "success": True,
            **upsert,
        }

    # ------------------------------------------------------------------
    # collect_api
    # ------------------------------------------------------------------

    async def _collect_api(self, **kwargs) -> dict[str, Any]:
        table_id = kwargs.get("table_id")
        records = kwargs.get("records")
        field_mapping = kwargs.get("field_mapping")
        pk_field_id = kwargs.get("primary_key_field_id")
        resume_from = kwargs.get("resume_from", 0)

        if not table_id:
            return {"success": False, "error": "Missing required field: table_id"}
        if not records:
            return {"success": False, "error": "Missing required field: records"}
        if not field_mapping:
            return {"success": False, "error": "Missing required field: field_mapping"}
        if not pk_field_id:
            return {"success": False, "error": "Missing required field: primary_key_field_id"}

        transformed = transform_records(records, field_mapping)
        if resume_from > 0:
            transformed = transformed[resume_from:]

        result = await self._batch_upsert(table_id, transformed, pk_field_id)
        return {"success": True, **result}

    # ------------------------------------------------------------------
    # upsert_records
    # ------------------------------------------------------------------

    async def _upsert_records(self, **kwargs) -> dict[str, Any]:
        table_id = kwargs.get("table_id")
        records = kwargs.get("records")
        pk_field_id = kwargs.get("primary_key_field_id")
        resume_from = kwargs.get("resume_from", 0)

        if not table_id:
            return {"success": False, "error": "Missing required field: table_id"}
        if not records:
            return {"success": False, "error": "Missing required field: records"}
        if not pk_field_id:
            return {"success": False, "error": "Missing required field: primary_key_field_id"}

        batch = records[resume_from:] if resume_from > 0 else records
        result = await self._batch_upsert(table_id, batch, pk_field_id)
        return {"success": True, **result}

    async def _batch_upsert(
        self, table_id: str, records: list[dict[str, Any]], pk_field_id: str
    ) -> dict[str, Any]:
        """Upsert records in batches of BATCH_SIZE via POST /tables/{id}/upsert."""
        client = await self._get_client()
        total = len(records)
        successful = 0
        errors: list[dict[str, Any]] = []

        for start in range(0, total, BATCH_SIZE):
            batch = records[start : start + BATCH_SIZE]
            try:
                resp = await client.post(
                    f"/tables/{table_id}/upsert",
                    json={
                        "records": batch,
                        "primary_key_field_id": pk_field_id,
                    },
                )
                resp.raise_for_status()
                data = resp.json()
                successful += data.get("inserted", 0) + data.get("updated", 0)
            except httpx.HTTPStatusError as e:
                errors.append(
                    {
                        "batch_start": start,
                        "batch_size": len(batch),
                        "status": e.response.status_code,
                        "error": e.response.text[:300],
                    }
                )
                break

        return {
            "successful_count": successful,
            "total": total,
            "resume_from": successful,
            **({"errors": errors} if errors else {}),
        }

    # ------------------------------------------------------------------
    # query_records
    # ------------------------------------------------------------------

    async def _query_records(self, **kwargs) -> dict[str, Any]:
        table_id = kwargs.get("table_id")
        if not table_id:
            return {"success": False, "error": "Missing required field: table_id"}

        client = await self._get_client()
        params: dict[str, Any] = {}
        if kwargs.get("cursor"):
            params["cursor"] = kwargs["cursor"]
        if kwargs.get("limit"):
            params["limit"] = kwargs["limit"]

        resp = await client.get(f"/tables/{table_id}/records", params=params)
        resp.raise_for_status()
        data = resp.json()
        return {
            "success": True,
            "records": data["records"],
            "next_cursor": data.get("next_cursor"),
        }
