# 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

"""
Workspace data layer for the dashboard.

Resolves workspace configs into live data: loads JSON files,
computes metrics, paginates tables, groups pipelines.
No LLM round-trips — pure file reads and computation.
"""

from __future__ import annotations

import json
import logging
import math
from datetime import date, datetime
from pathlib import Path
from typing import Any, Optional

logger = logging.getLogger(__name__)


def _get_data_dir() -> Path:
    try:
        from familiar.core.paths import DATA_DIR

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


# ---------------------------------------------------------------------------
# Data source registry
# ---------------------------------------------------------------------------


def _load_json(filename: str) -> dict:
    """Load a JSON file from the data directory."""
    path = _get_data_dir() / filename
    if not path.exists():
        return {}
    try:
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    except (json.JSONDecodeError, IOError, OSError) as e:
        logger.warning("Failed to load %s: %s", filename, e)
        return {}


def _load_calendar_data() -> dict:
    """Load today's calendar events via the calendar skill."""
    try:
        from familiar.skills.calendar.skill import get_events_structured

        events = get_events_structured({})
        # Flatten into workspace-friendly shape
        rows = []
        for ev in events:
            start = ev.get("start", "")
            # Extract time portion
            time_str = ""
            if "T" in str(start):
                try:
                    dt = datetime.fromisoformat(str(start).replace("Z", "+00:00"))
                    time_str = dt.strftime("%H:%M")
                except (ValueError, TypeError):
                    time_str = str(start)
            elif ev.get("all_day"):
                time_str = "All day"
            else:
                time_str = str(start)
            rows.append(
                {
                    "time": time_str,
                    "summary": ev.get("summary", ""),
                    "location": ev.get("location", ""),
                    "attendee_count": len(ev.get("attendees", [])),
                    "start_raw": str(start),
                }
            )
        return {"events": rows, "today_count": len(rows)}
    except Exception as e:
        logger.debug("Calendar data load failed: %s", e)
        return {"events": [], "today_count": 0}


def _load_email_summary() -> dict:
    """Load email summary counts (header-only scan, no LLM)."""
    try:
        from familiar.skills.triage.skill import CATEGORIES, get_category_summary

        # get_category_summary returns a text string; parse counts from it
        # Better approach: use the triage data directly
        text = get_category_summary({"days_back": 7})
        # Parse counts from text like "Category (N)" patterns
        counts = {"urgent_count": 0, "action_count": 0, "fyi_count": 0, "unread_count": 0}
        total = 0
        for line in text.split("\n"):
            line_lower = line.lower()
            for cat in CATEGORIES:
                if cat.lower() in line_lower:
                    # Try to extract number
                    for word in line.split():
                        stripped = word.strip("()")
                        if stripped.isdigit():
                            n = int(stripped)
                            total += n
                            if "urgent" in cat.lower():
                                counts["urgent_count"] = n
                            elif "action" in cat.lower():
                                counts["action_count"] = n
                            elif "fyi" in cat.lower() or "info" in cat.lower():
                                counts["fyi_count"] = n
                            break
        counts["unread_count"] = total
        return counts
    except Exception as e:
        logger.debug("Email summary load failed: %s", e)
        return {"urgent_count": 0, "action_count": 0, "fyi_count": 0, "unread_count": 0}


_WORKSPACE_SOURCES: dict[str, Any] = {
    "nonprofit": lambda: _load_json("nonprofit.json"),
    "bookkeeping": lambda: _load_json("bookkeeping.json"),
    "tasks": lambda: _load_tasks_data(),
    "contacts": lambda: _load_json("contacts.json"),
    "calendar": lambda: _load_calendar_data(),
    "email": lambda: _load_email_summary(),
    "cases": lambda: _load_cases_data(),
    "research": lambda: _load_json("research.json"),
    "kitchen": lambda: _load_kitchen_data(),
    "studio": lambda: _load_studio_data(),
}


def _load_tasks_data() -> dict:
    """Load tasks.json (which is an array) and wrap in a dict."""
    path = _get_data_dir() / "tasks.json"
    if not path.exists():
        return {"tasks": [], "pending_count": 0, "overdue_count": 0}
    try:
        with open(path, encoding="utf-8") as f:
            tasks = json.load(f)
        if isinstance(tasks, list):
            today = date.today().isoformat()
            pending = [t for t in tasks if not t.get("completed")]
            overdue = [t for t in pending if t.get("due_date") and t["due_date"] < today]
            return {
                "tasks": pending,
                "pending_count": len(pending),
                "overdue_count": len(overdue),
            }
        return {"tasks": [], "pending_count": 0, "overdue_count": 0}
    except (json.JSONDecodeError, IOError, OSError) as e:
        logger.warning("Failed to load tasks.json: %s", e)
        return {"tasks": [], "pending_count": 0, "overdue_count": 0}


def _load_cases_data() -> dict:
    """Load cases.json for the Social Worker workspace."""
    data = _load_json("cases.json")
    if not data:
        return {
            "cases": [],
            "resources": [],
            "notes": [],
            "crisis_log": [],
            "crisis_week_count": 0,
        }

    crisis_log = data.get("crisis_log", [])

    # Compute crisis-this-week count
    crisis_week_count = 0
    try:
        from datetime import timedelta

        week_ago = (date.today() - timedelta(days=7)).isoformat()
        crisis_week_count = sum(1 for c in crisis_log if c.get("date", "") >= week_ago)
    except Exception:
        pass

    data["crisis_week_count"] = crisis_week_count
    return data


def _load_kitchen_data() -> dict:
    """Load kitchen.json for the Chef workspace with computed metrics."""
    data = _load_json("kitchen.json")
    if not data:
        return {
            "recipes": [],
            "menus": [],
            "vendors": [],
            "inventory": [],
            "schedule": [],
            "costs": [],
            "food_cost_pct": 0,
            "labor_cost_pct": 0,
            "revenue_mtd": 0,
            "food_cost_mtd": 0,
            "labor_cost_mtd": 0,
            "avg_plate_cost": 0,
        }

    costs = data.get("costs", [])
    current_month = date.today().strftime("%Y-%m")
    mtd = [c for c in costs if str(c.get("date", "")).startswith(current_month)]

    revenue = sum(c.get("amount", 0) for c in mtd if c.get("type") == "revenue")
    food = sum(c.get("amount", 0) for c in mtd if c.get("type") == "food_cost")
    labor = sum(c.get("amount", 0) for c in mtd if c.get("type") == "labor_cost")

    data["revenue_mtd"] = revenue
    data["food_cost_mtd"] = food
    data["labor_cost_mtd"] = labor
    data["food_cost_pct"] = round((food / revenue * 100) if revenue else 0, 1)
    data["labor_cost_pct"] = round((labor / revenue * 100) if revenue else 0, 1)

    # Average plate cost from recipes
    recipes = data.get("recipes", [])
    costs_per = [r.get("cost_per_serving", 0) for r in recipes if r.get("cost_per_serving")]
    data["avg_plate_cost"] = round(sum(costs_per) / len(costs_per), 2) if costs_per else 0

    return data


def _load_studio_data() -> dict:
    """Load studio.json for the Artist workspace with computed metrics."""
    data = _load_json("studio.json")
    if not data:
        return {
            "pieces": [],
            "commissions": [],
            "shows": [],
            "materials": [],
            "clients": [],
            "revenue_mtd": 0,
        }

    # Revenue this month from sold pieces
    pieces = data.get("pieces", [])
    current_month = date.today().strftime("%Y-%m")
    data["revenue_mtd"] = sum(
        p.get("price", 0)
        for p in pieces
        if p.get("status") == "sold" and str(p.get("sold_date", "")).startswith(current_month)
    )

    return data


# ---------------------------------------------------------------------------
# Metric resolver
# ---------------------------------------------------------------------------


def resolve_metric(source_data: dict, metric_path: str) -> Any:
    """Resolve a dot-path metric from source data.

    Supported patterns:
        donors.count            -> len(data["donors"])
        grants.active_count     -> count where status in (applied, awarded)
        grants.deadline_count   -> count grants with upcoming deadlines
        donors.needs_thanks_count -> count donors needing thank-you
        ytd_income / ytd_expense / ytd_net -> sum transactions for current year
        donor_revenue / grant_revenue -> sum by category
        transaction_count       -> count of transactions
        pending_count / overdue_count -> task filtering
        today_count             -> calendar event count
        unread_count / urgent_count / action_count / fyi_count -> email counts
    """
    # Direct key match (flat metrics like pending_count, today_count)
    if metric_path in source_data:
        return source_data[metric_path]

    parts = metric_path.split(".")
    if len(parts) == 2:
        collection_key, agg = parts
        collection = source_data.get(collection_key, [])
        if not isinstance(collection, list):
            return 0

        if agg == "count":
            return len(collection)

        if agg == "active_count":
            # Grants: applied/awarded. Cases: assessment/active/follow_up.
            return sum(
                1
                for item in collection
                if item.get("status") in ("applied", "awarded", "assessment", "active", "follow_up")
            )

        if agg == "intake_count":
            return sum(1 for item in collection if item.get("status") == "intake")

        if agg == "followup_count":
            return sum(1 for item in collection if item.get("status") == "follow_up")

        # Kitchen: menus with status "active"
        if collection_key == "menus" and agg == "active_count":
            return sum(1 for item in collection if item.get("status") == "active")

        # Kitchen: inventory items below par level
        if agg == "low_stock_count":
            return sum(
                1
                for item in collection
                if item.get("quantity") is not None
                and item.get("par_level" if "par_level" in item else "reorder_at") is not None
                and item.get("quantity", 0) <= item.get("par_level", item.get("reorder_at", 0))
            )

        # Studio: pieces in active work stages
        if agg == "in_progress_count":
            return sum(
                1
                for item in collection
                if item.get("status")
                in ("concept", "in_progress", "drying", "bisque_fire", "glazing", "glaze_fire")
            )

        # Studio: open commissions (not completed/cancelled)
        if agg == "open_count":
            return sum(
                1
                for item in collection
                if item.get("status") not in ("completed", "cancelled", "delivered")
            )

        # Studio: upcoming shows (date >= today)
        if agg == "upcoming_count":
            today = date.today().isoformat()
            return sum(
                1
                for item in collection
                if item.get("date", "") >= today and item.get("status") not in ("cancelled",)
            )

        if agg == "deadline_count":
            today = date.today().isoformat()
            future_30 = None
            try:
                from datetime import timedelta

                future_30 = (date.today() + timedelta(days=30)).isoformat()
            except Exception:
                pass
            return sum(
                1
                for item in collection
                if item.get("deadline")
                and item.get("deadline") >= today
                and (future_30 is None or item.get("deadline") <= future_30)
                and item.get("status") not in ("declined", "completed")
            )

        if agg == "needs_thanks_count":
            count = 0
            for donor in collection:
                interactions = donor.get("interactions", [])
                # Find gifts not followed by a thank-you
                last_gift_idx = -1
                last_thanks_idx = -1
                for i, ix in enumerate(interactions):
                    if ix.get("type") == "gift":
                        last_gift_idx = i
                    if ix.get("type") == "thank_you":
                        last_thanks_idx = i
                if last_gift_idx >= 0 and last_gift_idx > last_thanks_idx:
                    count += 1
            return count

    # Bookkeeping metrics
    transactions = source_data.get("transactions", [])
    current_year = str(date.today().year)

    if metric_path == "ytd_income":
        return sum(
            t.get("amount", 0)
            for t in transactions
            if t.get("type") == "income" and str(t.get("date", "")).startswith(current_year)
        )

    if metric_path == "ytd_expense":
        return sum(
            t.get("amount", 0)
            for t in transactions
            if t.get("type") == "expense" and str(t.get("date", "")).startswith(current_year)
        )

    if metric_path == "ytd_net":
        income = resolve_metric(source_data, "ytd_income")
        expense = resolve_metric(source_data, "ytd_expense")
        return income - expense

    if metric_path == "donor_revenue":
        return sum(
            t.get("amount", 0)
            for t in transactions
            if t.get("type") == "income"
            and t.get("category") in ("individual_donations", "donations", "donor")
            and str(t.get("date", "")).startswith(current_year)
        )

    if metric_path == "grant_revenue":
        return sum(
            t.get("amount", 0)
            for t in transactions
            if t.get("type") == "income"
            and t.get("category") in ("grants", "grant")
            and str(t.get("date", "")).startswith(current_year)
        )

    if metric_path == "transaction_count":
        return len(transactions)

    return 0


# ---------------------------------------------------------------------------
# Section data fetcher
# ---------------------------------------------------------------------------


def get_section_data(
    section_config: dict,
    sort: Optional[str] = None,
    sort_dir: str = "asc",
    page: int = 1,
    limit: int = 50,
) -> dict:
    """Load and shape section data based on type (table, pipeline, summary)."""
    source_key = section_config.get("source", "")
    loader = _WORKSPACE_SOURCES.get(source_key)
    if not loader:
        return {"error": f"Unknown source: {source_key}"}

    try:
        source_data = loader()
    except Exception as e:
        logger.warning("Source %s load failed: %s", source_key, e)
        return {"error": str(e)}

    section_type = section_config.get("type", "table")

    if section_type == "table":
        return _build_table(section_config, source_data, sort, sort_dir, page, limit)
    elif section_type == "pipeline":
        return _build_pipeline(section_config, source_data)
    elif section_type == "summary":
        return _build_summary(section_config, source_data)
    else:
        return {"error": f"Unknown section type: {section_type}"}


def _build_table(
    config: dict,
    source_data: dict,
    sort: Optional[str],
    sort_dir: str,
    page: int,
    limit: int,
) -> dict:
    """Extract, sort, and paginate table rows."""
    data_key = config.get("source_key", "")
    rows = source_data.get(data_key, [])
    if not isinstance(rows, list):
        rows = []

    total = len(rows)

    # Sort
    sort_field = sort or config.get("default_sort", "")
    if sort_field:
        reverse = (sort_dir or config.get("default_sort_dir", "asc")) == "desc"
        rows = sorted(rows, key=lambda r: r.get(sort_field) or "", reverse=reverse)

    # Paginate
    pages = max(1, math.ceil(total / limit))
    page = max(1, min(page, pages))
    start = (page - 1) * limit
    rows = rows[start : start + limit]

    return {"rows": rows, "total": total, "page": page, "pages": pages}


def _build_pipeline(config: dict, source_data: dict) -> dict:
    """Group items by stage_field into pipeline columns."""
    data_key = config.get("source_key", "")
    items = source_data.get(data_key, [])
    if not isinstance(items, list):
        items = []

    stage_field = config.get("stage_field", "status")
    stages_order = config.get("stages", [])

    grouped: dict[str, list] = {s: [] for s in stages_order}
    for item in items:
        stage = item.get(stage_field, "")
        if stage in grouped:
            grouped[stage].append(item)
        elif stages_order:
            grouped.setdefault(stage, []).append(item)

    totals = {s: len(v) for s, v in grouped.items()}
    return {"stages": grouped, "totals": totals}


def _build_summary(config: dict, source_data: dict) -> dict:
    """Compute summary stats from config."""
    stats = []
    for stat_config in config.get("stats", []):
        metric = stat_config.get("metric", "")
        value = resolve_metric(source_data, metric)
        stats.append(
            {
                "key": stat_config.get("key", ""),
                "label": stat_config.get("label", ""),
                "icon": stat_config.get("icon", ""),
                "color": stat_config.get("color", ""),
                "value": value,
                "format": stat_config.get("format", "number"),
            }
        )
    return {"stats": stats}


# ---------------------------------------------------------------------------
# Overview card resolver
# ---------------------------------------------------------------------------


def resolve_overview_cards(overview_config: dict) -> list[dict]:
    """Resolve all overview card values."""
    cards = []
    # Cache source data to avoid re-loading
    source_cache: dict[str, dict] = {}

    for card in overview_config.get("cards", []):
        source_key = card.get("source", "")
        if source_key not in source_cache:
            loader = _WORKSPACE_SOURCES.get(source_key)
            if loader:
                try:
                    source_cache[source_key] = loader()
                except Exception as e:
                    logger.debug("Source %s failed for overview: %s", source_key, e)
                    source_cache[source_key] = {}
            else:
                source_cache[source_key] = {}

        value = resolve_metric(source_cache[source_key], card.get("metric", ""))
        cards.append(
            {
                "key": card.get("key", ""),
                "label": card.get("label", ""),
                "icon": card.get("icon", ""),
                "color": card.get("color", ""),
                "value": value,
                "format": card.get("format", "number"),
            }
        )
    return cards


# ---------------------------------------------------------------------------
# Action executor
# ---------------------------------------------------------------------------


def execute_action(action_config: dict, row_data: dict, agent: Any = None) -> dict:
    """Execute a workspace action (tool call or agent chat).

    Returns {"result": str, "success": bool}.
    """
    mode = action_config.get("mode", "agent")

    if mode == "tool":
        return _execute_tool_action(action_config, row_data)
    elif mode == "agent":
        return _execute_agent_action(action_config, row_data, agent)
    else:
        return {"result": f"Unknown action mode: {mode}", "success": False}


def _execute_tool_action(action_config: dict, row_data: dict) -> dict:
    """Call a tool handler directly via the tool registry."""
    tool_name = action_config.get("tool", "")
    tool_args = dict(action_config.get("tool_args", {}))
    # Merge row_data into tool_args
    tool_args.update(row_data)

    try:
        from familiar.core.tool_registry import get_tool

        tool_def = get_tool(tool_name)
        if tool_def and tool_def.handler:
            result = tool_def.handler(tool_args)
            return {"result": str(result), "success": True}
        return {"result": f"Tool '{tool_name}' not found", "success": False}
    except Exception as e:
        logger.warning("Tool action %s failed: %s", tool_name, e)
        return {"result": str(e), "success": False}


def _execute_agent_action(action_config: dict, row_data: dict, agent: Any) -> dict:
    """Route through agent.chat_with_results()."""
    prompt = action_config.get("prompt", "")
    template = action_config.get("prompt_template", "")

    if template and row_data:
        try:
            prompt = template.format(**row_data)
        except KeyError:
            prompt = template

    if not prompt:
        return {"result": "No prompt provided", "success": False}

    if not agent:
        return {"result": prompt, "success": True}

    try:
        response = agent.chat_with_results(prompt, user_id="dashboard", channel="workspace")
        return {"result": response.text, "success": True}
    except Exception as e:
        logger.warning("Agent action failed: %s", e)
        return {"result": str(e), "success": False}
