"""Spreadsheet tool: create, read, and modify CSV and XLSX files."""

from __future__ import annotations

import csv
import io
import json
from pathlib import Path
from typing import Any

from loguru import logger

from madpod_agent.agent.tools.base import Tool


def _resolve_path(path: str, workspace: Path, allowed_dir: Path | None) -> Path:
    """Resolve path against workspace and enforce directory restriction."""
    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 SpreadsheetTool(Tool):
    """Create, read, and modify CSV and XLSX spreadsheet files."""

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

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

    @property
    def description(self) -> str:
        return (
            "Create, read, or modify spreadsheet files (CSV and XLSX). "
            "Actions: 'create' (new file from data), 'read' (parse existing file), "
            "'append' (add rows to existing file), 'query' (filter/sort data). "
            "Returns structured JSON with the data."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["create", "read", "append", "query"],
                    "description": "Operation to perform",
                },
                "path": {
                    "type": "string",
                    "description": "File path (relative to workspace). Must end in .csv or .xlsx",
                },
                "headers": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Column headers (for 'create' action)",
                },
                "rows": {
                    "type": "array",
                    "items": {"type": "array"},
                    "description": "Data rows as arrays (for 'create' and 'append')",
                },
                "sheet": {
                    "type": "string",
                    "description": "Sheet name for XLSX files (default: first sheet)",
                },
                "max_rows": {
                    "type": "integer",
                    "description": "Max rows to return for 'read' (default: 500)",
                },
                "filter_column": {
                    "type": "string",
                    "description": "Column name to filter on (for 'query')",
                },
                "filter_value": {
                    "type": "string",
                    "description": "Value to filter for (for 'query')",
                },
                "sort_column": {
                    "type": "string",
                    "description": "Column name to sort by (for 'query')",
                },
                "sort_descending": {
                    "type": "boolean",
                    "description": "Sort descending (default: false)",
                },
            },
            "required": ["action", "path"],
        }

    async def execute(self, action: str, path: str = "", **kwargs: Any) -> str:
        # Accept file_path as alias for path (common LLM naming)
        if not path and "file_path" in kwargs:
            path = kwargs.pop("file_path")
        if not path:
            return json.dumps({"error": "path is required"})
        try:
            resolved = _resolve_path(path, self._workspace, self._allowed_dir)
        except PermissionError as exc:
            return json.dumps({"error": str(exc)})

        is_xlsx = resolved.suffix.lower() in (".xlsx", ".xls")
        is_csv = resolved.suffix.lower() == ".csv"

        if not is_xlsx and not is_csv:
            return json.dumps({"error": "File must be .csv or .xlsx"})

        if action == "create":
            return await self._create(resolved, is_xlsx, **kwargs)
        elif action == "read":
            return await self._read(resolved, is_xlsx, **kwargs)
        elif action == "append":
            return await self._append(resolved, is_xlsx, **kwargs)
        elif action == "query":
            return await self._query(resolved, is_xlsx, **kwargs)
        else:
            return json.dumps({"error": f"Unknown action: {action}"})

    async def _create(
        self, path: Path, is_xlsx: bool,
        headers: list[str] | None = None,
        rows: list[list[Any]] | None = None,
        sheet: str | None = None,
        **kwargs: Any,
    ) -> str:
        if not headers:
            return json.dumps({"error": "headers required for create action"})

        rows = rows or []
        path.parent.mkdir(parents=True, exist_ok=True)

        if is_xlsx:
            return self._create_xlsx(path, headers, rows, sheet)
        else:
            return self._create_csv(path, headers, rows)

    def _create_csv(self, path: Path, headers: list[str], rows: list[list[Any]]) -> str:
        with open(path, "w", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            writer.writerow(headers)
            writer.writerows(rows)

        logger.info("Created CSV: {} ({} rows)", path, len(rows))
        return json.dumps({
            "ok": True,
            "path": str(path),
            "format": "csv",
            "columns": len(headers),
            "rows": len(rows),
        })

    def _create_xlsx(
        self, path: Path, headers: list[str], rows: list[list[Any]], sheet: str | None,
    ) -> str:
        try:
            import openpyxl
        except ImportError:
            return json.dumps({"error": "openpyxl not installed. Use CSV format or install: pip install openpyxl"})

        wb = openpyxl.Workbook()
        ws = wb.active
        if sheet:
            ws.title = sheet
        ws.append(headers)
        for row in rows:
            ws.append(row)
        wb.save(path)

        logger.info("Created XLSX: {} ({} rows)", path, len(rows))
        return json.dumps({
            "ok": True,
            "path": str(path),
            "format": "xlsx",
            "sheet": ws.title,
            "columns": len(headers),
            "rows": len(rows),
        })

    async def _read(
        self, path: Path, is_xlsx: bool,
        max_rows: int = 500,
        sheet: str | None = None,
        **kwargs: Any,
    ) -> str:
        if not path.exists():
            return json.dumps({"error": f"File not found: {path.name}"})

        if is_xlsx:
            return self._read_xlsx(path, max_rows, sheet)
        else:
            return self._read_csv(path, max_rows)

    def _read_csv(self, path: Path, max_rows: int) -> str:
        with open(path, "r", encoding="utf-8", errors="replace") as f:
            reader = csv.reader(f)
            all_rows = list(reader)

        if not all_rows:
            return json.dumps({"ok": True, "headers": [], "rows": [], "total_rows": 0})

        headers = all_rows[0]
        data_rows = all_rows[1:]
        truncated = len(data_rows) > max_rows
        returned_rows = data_rows[:max_rows]

        return json.dumps({
            "ok": True,
            "path": str(path),
            "format": "csv",
            "headers": headers,
            "rows": returned_rows,
            "total_rows": len(data_rows),
            "truncated": truncated,
        })

    def _read_xlsx(self, path: Path, max_rows: int, sheet: str | None) -> str:
        try:
            import openpyxl
        except ImportError:
            return json.dumps({"error": "openpyxl not installed"})

        wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
        ws = wb[sheet] if sheet and sheet in wb.sheetnames else wb.active
        sheets = wb.sheetnames

        rows_iter = ws.iter_rows(values_only=True)
        headers_row = next(rows_iter, None)
        if headers_row is None:
            wb.close()
            return json.dumps({"ok": True, "headers": [], "rows": [], "total_rows": 0, "sheets": sheets})

        headers = [str(h) if h is not None else f"col_{i}" for i, h in enumerate(headers_row)]
        data_rows = []
        total = 0
        for row in rows_iter:
            total += 1
            if total <= max_rows:
                data_rows.append([_cell_to_json(c) for c in row])

        wb.close()
        return json.dumps({
            "ok": True,
            "path": str(path),
            "format": "xlsx",
            "sheet": ws.title,
            "sheets": sheets,
            "headers": headers,
            "rows": data_rows,
            "total_rows": total,
            "truncated": total > max_rows,
        }, default=str)

    async def _append(
        self, path: Path, is_xlsx: bool,
        rows: list[list[Any]] | None = None,
        sheet: str | None = None,
        **kwargs: Any,
    ) -> str:
        if not rows:
            return json.dumps({"error": "rows required for append action"})
        if not path.exists():
            return json.dumps({"error": f"File not found: {path.name}. Use 'create' first."})

        if is_xlsx:
            return self._append_xlsx(path, rows, sheet)
        else:
            return self._append_csv(path, rows)

    def _append_csv(self, path: Path, rows: list[list[Any]]) -> str:
        with open(path, "a", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            writer.writerows(rows)

        logger.info("Appended {} rows to CSV: {}", len(rows), path)
        return json.dumps({"ok": True, "appended_rows": len(rows), "path": str(path)})

    def _append_xlsx(self, path: Path, rows: list[list[Any]], sheet: str | None) -> str:
        try:
            import openpyxl
        except ImportError:
            return json.dumps({"error": "openpyxl not installed"})

        wb = openpyxl.load_workbook(path)
        ws = wb[sheet] if sheet and sheet in wb.sheetnames else wb.active
        for row in rows:
            ws.append(row)
        wb.save(path)

        logger.info("Appended {} rows to XLSX: {}", len(rows), path)
        return json.dumps({"ok": True, "appended_rows": len(rows), "path": str(path)})

    async def _query(
        self, path: Path, is_xlsx: bool,
        filter_column: str | None = None,
        filter_value: str | None = None,
        sort_column: str | None = None,
        sort_descending: bool = False,
        max_rows: int = 500,
        **kwargs: Any,
    ) -> str:
        if not path.exists():
            return json.dumps({"error": f"File not found: {path.name}"})

        read_result = json.loads(await self._read(path, is_xlsx, max_rows=10_000, **kwargs))
        if "error" in read_result:
            return json.dumps(read_result)

        headers = read_result["headers"]
        rows = read_result["rows"]

        # Filter
        if filter_column and filter_value is not None:
            if filter_column not in headers:
                return json.dumps({"error": f"Column '{filter_column}' not found. Available: {headers}"})
            col_idx = headers.index(filter_column)
            filter_lower = filter_value.lower()
            rows = [r for r in rows if len(r) > col_idx and str(r[col_idx]).lower().find(filter_lower) != -1]

        # Sort
        if sort_column:
            if sort_column not in headers:
                return json.dumps({"error": f"Column '{sort_column}' not found. Available: {headers}"})
            col_idx = headers.index(sort_column)
            rows.sort(key=lambda r: str(r[col_idx]) if len(r) > col_idx else "", reverse=sort_descending)

        truncated = len(rows) > max_rows
        returned = rows[:max_rows]

        return json.dumps({
            "ok": True,
            "headers": headers,
            "rows": returned,
            "matched_rows": len(rows),
            "truncated": truncated,
        }, default=str)


def _cell_to_json(cell: Any) -> Any:
    """Convert an openpyxl cell value to JSON-serializable type."""
    if cell is None:
        return None
    if isinstance(cell, (int, float, bool, str)):
        return cell
    return str(cell)
