# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Charts Skill

Generate charts and visualizations from data or existing Familiar datasets.
Bar, pie, line, and horizontal bar charts with consistent styling.

Output stored in ~/.familiar/documents/

Dependencies:
  - matplotlib (chart rendering) -- optional, auto-installed on first use
"""

import io
import json
import logging
import re
from datetime import datetime
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)

DOCUMENTS_DIR = Path.home() / ".familiar" / "documents"

MATPLOTLIB_AVAILABLE = False

try:
    import matplotlib

    matplotlib.use("Agg")
    import matplotlib.pyplot as plt

    MATPLOTLIB_AVAILABLE = True
except ImportError as e:
    logger.debug("Optional dependency not available: %s", e)

# Consistent color palette
COLOR_PALETTE = [
    "#2563EB",
    "#16A34A",
    "#DC2626",
    "#F59E0B",
    "#8B5CF6",
    "#06B6D4",
    "#EC4899",
    "#84CC16",
]


def _ensure_matplotlib():
    global MATPLOTLIB_AVAILABLE, matplotlib, plt
    if MATPLOTLIB_AVAILABLE:
        return True
    from familiar.core.deps import CHART_PACKAGES, ensure_packages

    ok, _ = ensure_packages(CHART_PACKAGES)
    if ok:
        import matplotlib as _mpl

        _mpl.use("Agg")
        import matplotlib.pyplot as _plt

        globals().update({"matplotlib": _mpl, "plt": _plt})
        MATPLOTLIB_AVAILABLE = True
    return MATPLOTLIB_AVAILABLE


def _get_data_dir():
    try:
        from familiar.core import paths

        return paths.DATA_DIR
    except ImportError:
        return Path.home() / ".familiar" / "data"


def _load_json(filename):
    path = _get_data_dir() / filename
    if path.exists():
        try:
            with open(path, encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError) as e:
            logger.debug("JSON parse error suppressed: %s", e)
    return {}


def _output_path(name: str, ext: str) -> Path:
    DOCUMENTS_DIR.mkdir(parents=True, exist_ok=True)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    safe_name = re.sub(r"[^\w\-]", "_", name)[:50]
    return DOCUMENTS_DIR / f"{safe_name}_{timestamp}.{ext}"


def _apply_familiar_style():
    """Apply consistent styling before each render."""
    plt.rcParams.update(
        {
            "font.family": "sans-serif",
            "font.sans-serif": ["Calibri", "DejaVu Sans", "Arial", "Helvetica"],
            "font.size": 11,
            "axes.titlesize": 14,
            "axes.labelsize": 12,
            "figure.facecolor": "white",
            "axes.facecolor": "white",
            "axes.grid": True,
            "grid.alpha": 0.3,
            "grid.linestyle": "--",
        }
    )


def _format_value(val, currency=False, percentage=False):
    """Format a value for display on charts."""
    if percentage:
        return f"{val:.1f}%"
    if currency:
        if val >= 1000:
            return f"${val:,.0f}"
        return f"${val:,.2f}"
    if isinstance(val, float):
        return f"{val:.1f}"
    return str(val)


def render_chart_bytes(
    chart_type: str,
    title: str,
    labels: list,
    values: Optional[list] = None,
    series: Optional[list] = None,
    x_label: str = "",
    y_label: str = "",
    currency: bool = False,
    percentage: bool = False,
    stacked: bool = False,
    show_values: bool = True,
    dpi: int = 150,
) -> Optional[bytes]:
    """Return PNG bytes for embedding in DOCX/PPTX. Called by documents skill."""
    if not _ensure_matplotlib():
        return None

    _apply_familiar_style()
    fig, ax = plt.subplots(figsize=(8, 5))
    colors = COLOR_PALETTE[: len(labels)]

    try:
        if chart_type == "pie":
            data = values or (series[0]["values"] if series else [])
            if not data:
                plt.close(fig)
                return None
            wedges, texts, autotexts = ax.pie(
                data, labels=labels, colors=colors, autopct="%1.1f%%", startangle=90
            )
            for t in autotexts:
                t.set_fontsize(9)
            ax.set_title(title)

        elif chart_type == "line":
            if series:
                for i, s in enumerate(series):
                    color = COLOR_PALETTE[i % len(COLOR_PALETTE)]
                    ax.plot(labels, s["values"], marker="o", color=color, label=s.get("name", ""))
                    if show_values:
                        for j, v in enumerate(s["values"]):
                            ax.annotate(
                                _format_value(v, currency, percentage),
                                (j, v),
                                textcoords="offset points",
                                xytext=(0, 8),
                                ha="center",
                                fontsize=8,
                            )
                ax.legend()
            else:
                ax.plot(labels, values, marker="o", color=COLOR_PALETTE[0])
                if show_values:
                    for i, v in enumerate(values):
                        ax.annotate(
                            _format_value(v, currency, percentage),
                            (i, v),
                            textcoords="offset points",
                            xytext=(0, 8),
                            ha="center",
                            fontsize=8,
                        )
            ax.set_title(title)
            if x_label:
                ax.set_xlabel(x_label)
            if y_label:
                ax.set_ylabel(y_label)

        elif chart_type == "hbar":
            data = values or (series[0]["values"] if series else [])
            if not data:
                plt.close(fig)
                return None
            y_pos = range(len(labels))
            ax.barh(y_pos, data, color=colors)
            ax.set_yticks(y_pos)
            ax.set_yticklabels(labels)
            if show_values:
                for i, v in enumerate(data):
                    ax.text(
                        v + max(data) * 0.01,
                        i,
                        _format_value(v, currency, percentage),
                        va="center",
                        fontsize=9,
                    )
            ax.set_title(title)
            if x_label:
                ax.set_xlabel(x_label)

        else:  # bar (default)
            if series and len(series) > 1:
                import numpy as np

                x = np.arange(len(labels))
                width = 0.8 / len(series)
                for i, s in enumerate(series):
                    offset = (i - len(series) / 2 + 0.5) * width
                    color = COLOR_PALETTE[i % len(COLOR_PALETTE)]
                    if stacked and i > 0:
                        bottom = [
                            sum(series[j]["values"][k] for j in range(i))
                            for k in range(len(labels))
                        ]
                        bars = ax.bar(
                            x,
                            s["values"],
                            width * len(series),
                            bottom=bottom,
                            color=color,
                            label=s.get("name", ""),
                        )
                    elif stacked:
                        bars = ax.bar(
                            x,
                            s["values"],
                            width * len(series),
                            color=color,
                            label=s.get("name", ""),
                        )
                    else:
                        bars = ax.bar(
                            x + offset,
                            s["values"],
                            width,
                            color=color,
                            label=s.get("name", ""),
                        )
                    if show_values:
                        for bar in bars:
                            h = bar.get_height()
                            ax.text(
                                bar.get_x() + bar.get_width() / 2,
                                bar.get_y() + h,
                                _format_value(h, currency, percentage),
                                ha="center",
                                va="bottom",
                                fontsize=8,
                            )
                ax.set_xticks(x)
                ax.set_xticklabels(labels)
                ax.legend()
            else:
                data = values or (series[0]["values"] if series else [])
                if not data:
                    plt.close(fig)
                    return None
                bars = ax.bar(labels, data, color=colors)
                if show_values:
                    for bar, v in zip(bars, data):
                        ax.text(
                            bar.get_x() + bar.get_width() / 2,
                            bar.get_height(),
                            _format_value(v, currency, percentage),
                            ha="center",
                            va="bottom",
                            fontsize=9,
                        )
            ax.set_title(title)
            if x_label:
                ax.set_xlabel(x_label)
            if y_label:
                ax.set_ylabel(y_label)

        plt.tight_layout()
        buf = io.BytesIO()
        fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight")
        plt.close(fig)
        buf.seek(0)
        return buf.read()

    except Exception as e:
        logger.warning("Chart rendering failed: %s", e)
        plt.close(fig)
        return None


# === Tool Handlers ===


def create_chart(data: dict) -> str:
    """Generate a chart from provided data."""
    if not _ensure_matplotlib():
        return "Chart generation unavailable — matplotlib could not be installed."

    chart_type = data.get("chart_type", "bar").lower()
    title = data.get("title", "Chart").strip()
    labels = data.get("labels", [])
    values = data.get("values")
    series = data.get("series")
    fmt = data.get("format", "png").lower()

    if not labels:
        return "Please provide labels for the chart."
    if not values and not series:
        return "Please provide values or series data for the chart."

    chart_bytes = render_chart_bytes(
        chart_type=chart_type,
        title=title,
        labels=labels,
        values=values,
        series=series,
        x_label=data.get("x_label", ""),
        y_label=data.get("y_label", ""),
        currency=data.get("currency", False),
        percentage=data.get("percentage", False),
        stacked=data.get("stacked", False),
        show_values=data.get("show_values", True),
    )

    if not chart_bytes:
        return "Failed to render chart."

    ext = "pdf" if fmt == "pdf" else "png"
    if fmt == "pdf":
        # Re-render as PDF
        _apply_familiar_style()
        path = _output_path(title, "pdf")
        # Rebuild for PDF format
        pdf_bytes = render_chart_bytes(
            chart_type=chart_type,
            title=title,
            labels=labels,
            values=values,
            series=series,
            x_label=data.get("x_label", ""),
            y_label=data.get("y_label", ""),
            currency=data.get("currency", False),
            percentage=data.get("percentage", False),
            stacked=data.get("stacked", False),
            show_values=data.get("show_values", True),
        )
        if pdf_bytes:
            path.write_bytes(pdf_bytes)
            return f"Chart created: {path}"
    else:
        path = _output_path(title, ext)
        path.write_bytes(chart_bytes)

    return f"Chart created: {path}"


def create_chart_from_data(data: dict) -> str:
    """Generate a chart from existing Familiar datasets."""
    if not _ensure_matplotlib():
        return "Chart generation unavailable — matplotlib could not be installed."

    dataset = data.get("dataset", "").strip().lower()
    fmt = data.get("format", "png").lower()
    period = data.get("period")

    dataset_configs = {
        "donor_tiers": {
            "chart_type": "pie",
            "title": "Donors by Giving Tier",
            "source": "nonprofit.json",
        },
        "income_vs_expense": {
            "chart_type": "bar",
            "title": "Income vs Expenses",
            "source": "bookkeeping.json",
        },
        "income_by_category": {
            "chart_type": "pie",
            "title": "Income by Category",
            "source": "bookkeeping.json",
        },
        "expense_by_category": {
            "chart_type": "pie",
            "title": "Expense by Category",
            "source": "bookkeeping.json",
        },
        "grant_pipeline": {
            "chart_type": "hbar",
            "title": "Grant Pipeline by Status",
            "source": "nonprofit.json",
        },
        "monthly_revenue": {
            "chart_type": "line",
            "title": "Monthly Revenue",
            "source": "bookkeeping.json",
        },
    }

    if dataset not in dataset_configs:
        available = ", ".join(dataset_configs.keys())
        return f"Unknown dataset '{dataset}'. Available: {available}"

    config = dataset_configs[dataset]
    source_data = _load_json(config["source"])

    labels, values = [], []

    if dataset == "donor_tiers":
        donors = source_data.get("donors", [])
        if not donors:
            return "No donor data found."
        tiers = {"Major ($10K+)": 0, "Mid-Level ($1K+)": 0, "Supporter ($100+)": 0, "Donor": 0}
        for d in donors:
            total = d.get("total_given", 0)
            if total >= 10000:
                tiers["Major ($10K+)"] += 1
            elif total >= 1000:
                tiers["Mid-Level ($1K+)"] += 1
            elif total >= 100:
                tiers["Supporter ($100+)"] += 1
            elif total > 0:
                tiers["Donor"] += 1
        tiers = {k: v for k, v in tiers.items() if v > 0}
        labels, values = list(tiers.keys()), list(tiers.values())

    elif dataset == "income_vs_expense":
        txs = source_data.get("transactions", [])
        if period:
            txs = [t for t in txs if t.get("date", "").startswith(period)]
        if not txs:
            return "No transaction data found."
        income = sum(t["amount"] for t in txs if t.get("type") == "income")
        expense = sum(t["amount"] for t in txs if t.get("type") == "expense")
        labels, values = ["Income", "Expenses"], [income, expense]

    elif dataset in ("income_by_category", "expense_by_category"):
        txs = source_data.get("transactions", [])
        if period:
            txs = [t for t in txs if t.get("date", "").startswith(period)]
        tx_type = "income" if "income" in dataset else "expense"
        filtered = [t for t in txs if t.get("type") == tx_type]
        if not filtered:
            return f"No {tx_type} transactions found."
        cats = {}
        for t in filtered:
            cat = t.get("category", "Other")
            cats[cat] = cats.get(cat, 0) + t["amount"]
        sorted_cats = sorted(cats.items(), key=lambda x: -x[1])
        labels = [c[0] for c in sorted_cats]
        values = [c[1] for c in sorted_cats]

    elif dataset == "grant_pipeline":
        grants = source_data.get("grants", [])
        if not grants:
            return "No grant data found."
        statuses = {}
        for g in grants:
            status = g.get("status", "prospect")
            statuses[status] = statuses.get(status, 0) + (g.get("amount", 0) or 0)
        order = ["prospect", "applied", "awarded", "completed", "declined"]
        labels = [s for s in order if s in statuses]
        values = [statuses[s] for s in labels]

    elif dataset == "monthly_revenue":
        txs = source_data.get("transactions", [])
        year = period or str(datetime.now().year)
        txs = [t for t in txs if t.get("date", "").startswith(year) and t.get("type") == "income"]
        if not txs:
            return f"No income transactions found for {year}."
        months = {}
        for t in txs:
            month = t.get("date", "")[:7]
            months[month] = months.get(month, 0) + t["amount"]
        sorted_months = sorted(months.items())
        labels = [m[0] for m in sorted_months]
        values = [m[1] for m in sorted_months]

    if not labels:
        return f"No data available for {dataset}."

    chart_bytes = render_chart_bytes(
        chart_type=config["chart_type"],
        title=config["title"],
        labels=labels,
        values=values,
        currency=dataset != "donor_tiers",
        show_values=True,
    )

    if not chart_bytes:
        return "Failed to render chart."

    path = _output_path(dataset, "pdf" if fmt == "pdf" else "png")
    path.write_bytes(chart_bytes)
    return f"Chart created: {path}"


# === Tool Definitions ===

TOOLS = [
    {
        "name": "create_chart",
        "description": (
            "Generate a chart or graph from provided data. Supports bar, pie, line, and "
            "horizontal bar chart types. Data can be simple labels/values or multi-series "
            "for grouped/stacked charts. Output as PNG or PDF saved to ~/.familiar/documents/."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "chart_type": {
                    "type": "string",
                    "enum": ["bar", "pie", "line", "hbar"],
                    "default": "bar",
                    "description": "Type of chart to generate",
                },
                "title": {"type": "string", "description": "Chart title"},
                "labels": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Category labels or x-axis values",
                },
                "values": {
                    "type": "array",
                    "items": {"type": "number"},
                    "description": "Data values (for single-series charts)",
                },
                "series": {
                    "type": "array",
                    "description": "Multi-series data, each with name and values",
                    "items": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string"},
                            "values": {"type": "array", "items": {"type": "number"}},
                        },
                    },
                },
                "x_label": {"type": "string", "description": "X-axis label"},
                "y_label": {"type": "string", "description": "Y-axis label"},
                "format": {
                    "type": "string",
                    "enum": ["png", "pdf"],
                    "default": "png",
                },
                "currency": {
                    "type": "boolean",
                    "default": False,
                    "description": "Format values as currency",
                },
                "percentage": {
                    "type": "boolean",
                    "default": False,
                    "description": "Format values as percentages",
                },
                "stacked": {
                    "type": "boolean",
                    "default": False,
                    "description": "Stack multi-series bars",
                },
                "show_values": {
                    "type": "boolean",
                    "default": True,
                    "description": "Show value labels on chart",
                },
            },
            "required": ["title", "labels"],
        },
        "input_examples": [
            {
                "chart_type": "bar",
                "title": "Q1 Revenue by Month",
                "labels": ["Jan", "Feb", "Mar"],
                "values": [12000, 15000, 18000],
                "currency": True,
            },
        ],
        "handler": create_chart,
        "category": "charts",
    },
    {
        "name": "create_chart_from_data",
        "description": (
            "Generate a chart from existing Familiar datasets. Automatically selects the "
            "best chart type and reads from nonprofit or bookkeeping data. "
            "Supported datasets: donor_tiers, income_vs_expense, income_by_category, "
            "expense_by_category, grant_pipeline, monthly_revenue."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "dataset": {
                    "type": "string",
                    "enum": [
                        "donor_tiers",
                        "income_vs_expense",
                        "income_by_category",
                        "expense_by_category",
                        "grant_pipeline",
                        "monthly_revenue",
                    ],
                    "description": "Which dataset to visualize",
                },
                "period": {
                    "type": "string",
                    "description": "Filter by year or year-month (e.g. '2026' or '2026-01')",
                },
                "format": {
                    "type": "string",
                    "enum": ["png", "pdf"],
                    "default": "png",
                },
            },
            "required": ["dataset"],
        },
        "handler": create_chart_from_data,
        "category": "charts",
    },
]
