# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# Licensed under the MIT License

"""Nonprofit Director routes — /api/nonprofit/*, /api/bookkeeping/*, /api/legislation/*, /api/grants-gov/*."""

from __future__ import annotations

import json
import logging
import os
from datetime import datetime
from pathlib import Path

from flask import Blueprint, Response, jsonify, request, send_file

from ..auth import require_auth
from ..helpers import get_agent, DATA_DIR, safe_error

logger = logging.getLogger(__name__)

bp = Blueprint("nonprofit", __name__)


# Nonprofit CRUD API
# ============================================================


@bp.route("/api/nonprofit/donors")
@require_auth
def api_nonprofit_donors():
    """List all donors."""
    from familiar.skills.nonprofit.skill import get_all_donors

    return jsonify({"donors": get_all_donors()})


@bp.route("/api/nonprofit/donors", methods=["POST"])
@require_auth
def api_nonprofit_donor_add():
    """Create a new donor."""
    data = request.json or {}
    if not data.get("name", "").strip():
        return jsonify({"error": "Name is required."}), 400

    from familiar.skills.nonprofit.skill import log_donor

    log_donor(data)

    # Return the newly created donor
    from familiar.skills.nonprofit.skill import get_all_donors

    donors = get_all_donors()
    # Find by name match (just created)
    created = next(
        (d for d in donors if d["name"].lower() == data["name"].strip().lower()),
        donors[-1] if donors else None,
    )
    return jsonify({"donor": created}), 201


@bp.route("/api/nonprofit/donors/<donor_id>", methods=["PUT"])
@require_auth
def api_nonprofit_donor_update(donor_id):
    """Update a donor."""
    data = request.json or {}
    data["id"] = donor_id

    from familiar.skills.nonprofit.skill import update_donor

    result = update_donor(data)
    if "error" in result:
        return jsonify(result), 404
    return jsonify({"donor": result})


@bp.route("/api/nonprofit/donors/<donor_id>", methods=["DELETE"])
@require_auth
def api_nonprofit_donor_delete(donor_id):
    """Delete a donor."""
    from familiar.skills.nonprofit.skill import delete_donor

    result = delete_donor(donor_id)
    if "error" in result:
        return jsonify(result), 404
    return jsonify(result)


@bp.route("/api/nonprofit/donors/<donor_id>/interaction", methods=["POST"])
@require_auth
def api_nonprofit_donor_interaction(donor_id):
    """Log an interaction for a donor."""
    data = request.json or {}
    data["donor_id"] = donor_id

    from familiar.skills.nonprofit.skill import add_interaction

    result = add_interaction(data)
    if "error" in result:
        return jsonify(result), 404
    return jsonify({"donor": result})


@bp.route("/api/nonprofit/grants")
@require_auth
def api_nonprofit_grants():
    """List all grants."""
    from familiar.skills.nonprofit.skill import get_all_grants

    return jsonify({"grants": get_all_grants()})


@bp.route("/api/nonprofit/grants", methods=["POST"])
@require_auth
def api_nonprofit_grant_add():
    """Create a new grant."""
    data = request.json or {}
    if not data.get("name", "").strip():
        return jsonify({"error": "Grant name is required."}), 400

    from familiar.skills.nonprofit.skill import add_grant

    add_grant(data)

    from familiar.skills.nonprofit.skill import get_all_grants

    grants = get_all_grants()
    return jsonify({"grant": grants[-1] if grants else None}), 201


@bp.route("/api/nonprofit/grants/<grant_id>", methods=["PUT"])
@require_auth
def api_nonprofit_grant_update(grant_id):
    """Update a grant."""
    data = request.json or {}
    data["id"] = grant_id

    from familiar.skills.nonprofit.skill import update_grant

    result = update_grant(data)
    if "error" in result:
        return jsonify(result), 404
    return jsonify({"grant": result})


@bp.route("/api/nonprofit/grants/<grant_id>", methods=["DELETE"])
@require_auth
def api_nonprofit_grant_delete(grant_id):
    """Delete a grant."""
    from familiar.skills.nonprofit.skill import delete_grant

    result = delete_grant(grant_id)
    if "error" in result:
        return jsonify(result), 404
    return jsonify(result)


@bp.route("/api/nonprofit/notes")
@require_auth
def api_nonprofit_notes():
    """List all notes."""
    from familiar.skills.nonprofit.skill import get_all_notes

    return jsonify({"notes": get_all_notes()})


@bp.route("/api/nonprofit/notes", methods=["POST"])
@require_auth
def api_nonprofit_note_add():
    """Create a new note."""
    data = request.json or {}
    if not data.get("content", "").strip():
        return jsonify({"error": "Note content is required."}), 400

    from familiar.skills.nonprofit.skill import add_note

    add_note(data)

    from familiar.skills.nonprofit.skill import get_all_notes

    notes = get_all_notes()
    return jsonify({"note": notes[-1] if notes else None}), 201


@bp.route("/api/nonprofit/notes/<note_id>", methods=["DELETE"])
@require_auth
def api_nonprofit_note_delete(note_id):
    """Delete a note."""
    from familiar.skills.nonprofit.skill import delete_note

    result = delete_note(note_id)
    if "error" in result:
        return jsonify(result), 404
    return jsonify(result)


# ============================================================
# Bookkeeping CRUD API
# ============================================================


@bp.route("/api/bookkeeping/transactions")
@require_auth
def api_bookkeeping_transactions():
    """List transactions with optional filters."""
    from familiar.skills.bookkeeping.skill import get_all_transactions

    filters = {
        "start_date": request.args.get("start_date"),
        "end_date": request.args.get("end_date"),
        "category": request.args.get("category"),
        "fund": request.args.get("fund"),
        "type": request.args.get("type"),
    }
    # Remove None values
    filters = {k: v for k, v in filters.items() if v}
    txns = get_all_transactions(filters if filters else None)
    limit = request.args.get("limit", 200, type=int)
    return jsonify({"transactions": txns[:limit]})


@bp.route("/api/bookkeeping/transactions", methods=["POST"])
@require_auth
def api_bookkeeping_transaction_add():
    """Create a new transaction (income or expense)."""
    data = request.json or {}
    if not data.get("amount"):
        return jsonify({"error": "Amount is required."}), 400

    tx_type = data.get("type", "income")
    if tx_type == "expense":
        from familiar.skills.bookkeeping.skill import log_expense

        log_expense(data)
    else:
        from familiar.skills.bookkeeping.skill import log_income

        log_income(data)

    from familiar.skills.bookkeeping.skill import get_all_transactions

    txns = get_all_transactions()
    return jsonify({"transaction": txns[0] if txns else None}), 201


@bp.route("/api/bookkeeping/transactions/<tx_id>", methods=["PUT"])
@require_auth
def api_bookkeeping_transaction_update(tx_id):
    """Update a transaction."""
    data = request.json or {}
    data["id"] = tx_id

    from familiar.skills.bookkeeping.skill import update_transaction

    result = update_transaction(data)
    if "error" in result:
        return jsonify(result), 404
    return jsonify({"transaction": result})


@bp.route("/api/bookkeeping/transactions/<tx_id>", methods=["DELETE"])
@require_auth
def api_bookkeeping_transaction_delete(tx_id):
    """Delete a transaction."""
    from familiar.skills.bookkeeping.skill import delete_transaction

    result = delete_transaction(tx_id)
    if "error" in result:
        return jsonify(result), 404
    return jsonify(result)


@bp.route("/api/bookkeeping/categories")
@require_auth
def api_bookkeeping_categories():
    """Get income/expense category lists."""
    from familiar.skills.bookkeeping.skill import get_categories

    return jsonify({"categories": get_categories()})


@bp.route("/api/bookkeeping/funds")
@require_auth
def api_bookkeeping_funds():
    """Get fund list."""
    from familiar.skills.bookkeeping.skill import get_funds

    return jsonify({"funds": get_funds()})


@bp.route("/api/nonprofit/import", methods=["POST"])
@require_auth
def api_nonprofit_import():
    """Import donors from CSV data.

    Expects JSON body: {"rows": [{"name": "...", "email": "...", ...}, ...]}
    or a CSV file upload via multipart form.
    """
    from familiar.skills.nonprofit.skill import log_donor

    rows = []
    if request.is_json:
        rows = (request.json or {}).get("rows", [])
    elif request.files.get("file"):
        import csv
        import io

        f = request.files["file"]
        text = f.read().decode("utf-8-sig")
        reader = csv.DictReader(io.StringIO(text))
        rows = list(reader)

    if not rows:
        return jsonify({"error": "No data provided."}), 400

    imported = 0
    errors = []
    for i, row in enumerate(rows):
        name = (row.get("name") or row.get("Name") or "").strip()
        if not name:
            errors.append(f"Row {i + 1}: missing name")
            continue

        donor_data = {
            "name": name,
            "email": row.get("email") or row.get("Email") or "",
            "phone": row.get("phone") or row.get("Phone") or "",
            "notes": row.get("notes") or row.get("Notes") or "",
        }

        # Handle tags (comma-separated string or list)
        tags = row.get("tags") or row.get("Tags") or ""
        if isinstance(tags, str) and tags:
            donor_data["tags"] = [t.strip() for t in tags.split(",") if t.strip()]

        # If there's an amount, log as gift
        amount = row.get("amount") or row.get("Amount") or row.get("total_given") or ""
        if amount:
            try:
                # Strip currency symbols
                amount_clean = str(amount).replace("$", "").replace(",", "").strip()
                donor_data["amount"] = float(amount_clean)
                donor_data["type"] = "gift"
                donor_data["description"] = row.get("description") or "Imported"
            except (ValueError, TypeError):
                pass

        log_donor(donor_data)
        imported += 1

    return jsonify(
        {
            "imported": imported,
            "errors": errors,
            "total_rows": len(rows),
        }
    )


# ============================================================
# Financial Charts API
# ============================================================


@bp.route("/api/bookkeeping/chart/<chart_type>")
@require_auth
def api_bookkeeping_chart(chart_type):
    """Generate a financial chart as base64 PNG.

    Chart types:
      - monthly: income vs expense bar chart by month (current year)
      - category_income: pie chart of income by category
      - category_expense: pie chart of expense by category
      - fund: bar chart of fund balances
    """
    import base64
    from datetime import datetime as _dt

    from familiar.skills.bookkeeping.skill import get_all_transactions

    year = request.args.get("year", str(_dt.now().year))
    txns = get_all_transactions({"start_date": f"{year}-01-01", "end_date": f"{year}-12-31"})

    try:
        from familiar.skills.charts.skill import render_chart_bytes
    except ImportError:
        return jsonify({"error": "Charts skill not available (matplotlib not installed)."}), 501

    chart_bytes = None

    if chart_type == "monthly":
        # Monthly income vs expenses
        months = {}
        for t in txns:
            m = t.get("date", "")[:7]  # "2026-01"
            if m not in months:
                months[m] = {"income": 0, "expense": 0}
            months[m][t["type"]] += t["amount"]
        sorted_months = sorted(months.keys())
        if not sorted_months:
            return jsonify({"error": "No transactions for this year."})
        labels = [m[5:] for m in sorted_months]  # "01", "02", etc.
        month_names = {
            "01": "Jan",
            "02": "Feb",
            "03": "Mar",
            "04": "Apr",
            "05": "May",
            "06": "Jun",
            "07": "Jul",
            "08": "Aug",
            "09": "Sep",
            "10": "Oct",
            "11": "Nov",
            "12": "Dec",
        }
        labels = [month_names.get(l, l) for l in labels]
        chart_bytes = render_chart_bytes(
            chart_type="bar",
            title=f"Income vs Expenses — {year}",
            labels=labels,
            series=[
                {"name": "Income", "values": [months[m]["income"] for m in sorted_months]},
                {"name": "Expenses", "values": [months[m]["expense"] for m in sorted_months]},
            ],
            y_label="Amount ($)",
            currency=True,
        )

    elif chart_type == "category_income":
        cats = {}
        for t in txns:
            if t["type"] == "income":
                cat = t.get("category", "other").replace("_", " ").title()
                cats[cat] = cats.get(cat, 0) + t["amount"]
        if not cats:
            return jsonify({"error": "No income transactions."})
        labels = list(cats.keys())
        values = list(cats.values())
        chart_bytes = render_chart_bytes(
            chart_type="pie",
            title=f"Income by Category — {year}",
            labels=labels,
            values=values,
            currency=True,
        )

    elif chart_type == "category_expense":
        cats = {}
        for t in txns:
            if t["type"] == "expense":
                cat = t.get("category", "other").replace("_", " ").title()
                cats[cat] = cats.get(cat, 0) + t["amount"]
        if not cats:
            return jsonify({"error": "No expense transactions."})
        labels = list(cats.keys())
        values = list(cats.values())
        chart_bytes = render_chart_bytes(
            chart_type="pie",
            title=f"Expenses by Category — {year}",
            labels=labels,
            values=values,
            currency=True,
        )

    elif chart_type == "fund":
        funds = {}
        for t in txns:
            fund = t.get("fund", "General")
            if fund not in funds:
                funds[fund] = {"income": 0, "expense": 0}
            funds[fund][t["type"]] += t["amount"]
        if not funds:
            return jsonify({"error": "No transactions."})
        labels = list(funds.keys())
        chart_bytes = render_chart_bytes(
            chart_type="bar",
            title=f"Fund Balances — {year}",
            labels=labels,
            series=[
                {"name": "Income", "values": [funds[f]["income"] for f in labels]},
                {"name": "Expenses", "values": [funds[f]["expense"] for f in labels]},
            ],
            y_label="Amount ($)",
            currency=True,
        )

    else:
        return jsonify({"error": f"Unknown chart type: {chart_type}"}), 400

    if not chart_bytes:
        return jsonify({"error": "Chart rendering failed — matplotlib may not be installed."}), 501

    b64 = base64.b64encode(chart_bytes).decode("ascii")
    return jsonify({"image": f"data:image/png;base64,{b64}", "chart_type": chart_type})


# ============================================================
# Grants.gov Search API
# ============================================================


@bp.route("/api/grants-gov/search")
@require_auth
def api_grants_gov_search():
    """Search Grants.gov for federal funding opportunities."""
    keyword = request.args.get("keyword", "") or request.args.get("q", "")
    agency = request.args.get("agency", "")
    category = request.args.get("category", "")
    date_range = request.args.get("dateRange", "")  # posted within last N days
    sort_by = request.args.get("sortBy", "")  # closeDateAsc, closeDateDesc, openDate, etc.
    limit = min(int(request.args.get("limit", "25")), 100)
    offset = int(request.args.get("offset", "0"))

    if not keyword and not agency and not category:
        return jsonify({"error": "Provide keyword, agency, or category"}), 400

    try:
        from familiar.skills._api_utils import api_post
        from familiar.skills.grants_gov.skill import search_grants as _search_grants  # noqa: F401

        body = {
            "paging": {"num": limit, "offset": offset},
            "criteria": {"oppStatuses": "forecasted|posted"},
        }
        if keyword:
            body["criteria"]["keyword"] = keyword
        if agency:
            body["criteria"]["agency"] = agency
        if category:
            body["criteria"]["fundingCategory"] = category
        if date_range:
            body["criteria"]["dateRange"] = date_range
        if sort_by:
            body["criteria"]["sortBy"] = sort_by

        result = api_post(
            "https://api.grants.gov/v1/api/search2",
            json=body,
            headers={"Content-Type": "application/json"},
            service_name="Grants.gov",
        )
        search_data = result.get("data", result)
        opportunities = search_data.get("oppHits", [])
        grants = []
        for opp in opportunities:
            grants.append(
                {
                    "id": opp.get("id", opp.get("number", "")),
                    "title": opp.get("title", "Untitled"),
                    "agency": opp.get("agency", ""),
                    "open_date": opp.get("openDate", ""),
                    "close_date": opp.get("closeDate", ""),
                    "award_floor": opp.get("awardFloor", 0),
                    "award_ceiling": opp.get("awardCeiling", 0),
                    "number": opp.get("number", ""),
                    "status": opp.get("oppStatus", ""),
                }
            )
        total = search_data.get("hitCount", search_data.get("totalCount", len(grants)))
        return jsonify(
            {
                "grants": grants,
                "total": total,
                "offset": offset,
                "limit": limit,
                "has_more": (offset + limit) < total,
            }
        )
    except Exception as e:
        return jsonify({"error": safe_error(e)}), 502


# ============================================================
# Legislation Search + Tracking API
# ============================================================


@bp.route("/api/legislation/search")
@require_auth
def api_legislation_search():
    """Search Congress.gov for federal bills."""
    query = request.args.get("query", "")
    congress = request.args.get("congress", "119")  # Current congress
    sort = request.args.get("sort", "updateDate+desc")
    limit = min(int(request.args.get("limit", "25")), 100)
    offset = int(request.args.get("offset", "0"))

    if not query:
        return jsonify({"error": "Provide a search query"}), 400

    try:
        import urllib.error
        import urllib.request

        api_key = os.environ.get("CONGRESS_API_KEY", "DEMO_KEY")
        params = (
            f"query={urllib.parse.quote(query)}"
            f"&limit={limit}&offset={offset}"
            f"&sort={urllib.parse.quote(sort)}"
            f"&format=json&api_key={api_key}"
        )
        url = f"https://api.congress.gov/v3/bill/{congress}?{params}"

        req = urllib.request.Request(
            url,
            headers={
                "Accept": "application/json",
                "User-Agent": "Familiar-Agent/1.15 (nonprofit-tool)",
            },
        )
        with urllib.request.urlopen(req, timeout=15) as resp:
            data = json.loads(resp.read())

        bills = []
        for b in data.get("bills", []):
            latest = b.get("latestAction", {})
            bills.append(
                {
                    "bill_id": f"{b.get('type', '')}{b.get('number', '')}",
                    "title": b.get("title", ""),
                    "chamber": b.get("originChamber", ""),
                    "congress": str(b.get("congress", congress)),
                    "latest_action": latest.get("text", ""),
                    "latest_action_date": latest.get("actionDate", ""),
                    "url": b.get("url", "")
                    .replace("format=json", "")
                    .replace("api_key=" + api_key, "")
                    .rstrip("?&"),
                    "type": b.get("type", ""),
                    "number": str(b.get("number", "")),
                    "update_date": b.get("updateDate", ""),
                }
            )

        pagination = data.get("pagination", {})
        total = pagination.get("count", len(bills))

        return jsonify(
            {
                "bills": bills,
                "total": total,
                "offset": offset,
                "limit": limit,
                "has_more": (offset + limit) < total,
            }
        )
    except Exception as e:
        return jsonify({"error": safe_error(e)}), 502


@bp.route("/api/legislation/search/state")
@require_auth
def api_legislation_search_state():
    """Search state bills via OpenStates or LegiScan.

    Tries OpenStates first (OPENSTATES_API_KEY), falls back to LegiScan
    (LEGISCAN_API_KEY). Returns 501 if neither key is configured.
    """
    query = request.args.get("query", "")
    state = request.args.get("state", "")
    limit = min(int(request.args.get("limit", "25")), 50)
    page = int(request.args.get("page", "1"))

    if not query:
        return jsonify({"error": "Provide a search query"}), 400
    if not state:
        return jsonify({"error": "Provide a state (e.g. WA, CA, NY)"}), 400

    import urllib.error
    import urllib.request

    # --- Try OpenStates first ---
    os_key = os.environ.get("OPENSTATES_API_KEY", "")
    if os_key:
        try:
            params = (
                f"jurisdiction={urllib.parse.quote(state)}"
                f"&q={urllib.parse.quote(query)}"
                f"&page={page}&per_page={limit}"
                f"&sort=updated_desc"
                f"&apikey={os_key}"
            )
            url = f"https://v3.openstates.org/bills?{params}"
            req = urllib.request.Request(
                url,
                headers={
                    "Accept": "application/json",
                    "User-Agent": "Familiar-Agent/1.15 (nonprofit-tool)",
                },
            )
            with urllib.request.urlopen(req, timeout=15) as resp:
                data = json.loads(resp.read())

            bills = []
            for b in data.get("results", []):
                latest = b.get("latest_action_description") or ""
                latest_date = (b.get("latest_action_date") or "")[:10]
                bills.append(
                    {
                        "bill_id": b.get("identifier", ""),
                        "title": b.get("title", ""),
                        "chamber": b.get("from_organization", {}).get("name", ""),
                        "congress": b.get("session", ""),
                        "state": state.upper(),
                        "level": "state",
                        "latest_action": latest,
                        "latest_action_date": latest_date,
                        "url": b.get("openstates_url", ""),
                        "sponsor": (b["sponsorships"][0]["name"] if b.get("sponsorships") else ""),
                        "update_date": (b.get("updated_at") or "")[:10],
                    }
                )
            pagination = data.get("pagination", {})
            total = pagination.get("total_items", len(bills))
            max_page = pagination.get("max_page", 1)
            return jsonify(
                {
                    "bills": bills,
                    "total": total,
                    "page": page,
                    "limit": limit,
                    "has_more": page < max_page,
                    "source": "openstates",
                }
            )
        except Exception as e:
            logger.warning("OpenStates search failed: %s", e)
            # Fall through to LegiScan

    # --- Try LegiScan ---
    ls_key = os.environ.get("LEGISCAN_API_KEY", "")
    if ls_key:
        try:
            params = (
                f"key={ls_key}&op=searchRaw"
                f"&state={urllib.parse.quote(state.upper())}"
                f"&query={urllib.parse.quote(query)}"
                f"&page={page}"
            )
            url = f"https://api.legiscan.com/?{params}"
            req = urllib.request.Request(
                url,
                headers={
                    "User-Agent": "Familiar-Agent/1.15 (nonprofit-tool)",
                },
            )
            with urllib.request.urlopen(req, timeout=15) as resp:
                data = json.loads(resp.read())

            if data.get("status") == "ERROR":
                return jsonify(
                    {"error": data.get("alert", {}).get("message", "LegiScan error")}
                ), 502

            search_result = data.get("searchresult", {})
            # LegiScan returns {summary: {}, 0: {}, 1: {}, ...}
            summary = search_result.pop("summary", {})
            total = summary.get("count", 0)

            bills = []
            for _key, b in sorted(search_result.items()):
                if not isinstance(b, dict) or "bill_number" not in b:
                    continue
                bills.append(
                    {
                        "bill_id": b.get("bill_number", ""),
                        "title": b.get("title", ""),
                        "chamber": "",
                        "congress": str(b.get("session_id", "")),
                        "state": b.get("state", state.upper()),
                        "level": "state",
                        "latest_action": b.get("last_action", ""),
                        "latest_action_date": b.get("last_action_date", ""),
                        "url": b.get("url", ""),
                        "sponsor": "",
                        "update_date": b.get("last_action_date", ""),
                    }
                )

            return jsonify(
                {
                    "bills": bills,
                    "total": total,
                    "page": page,
                    "limit": limit,
                    "has_more": page * 50 < total,  # LegiScan pages are 50 items
                    "source": "legiscan",
                }
            )
        except Exception as e:
            logger.warning("LegiScan search failed: %s", e)

    # --- Fallback: WA state has a free API (no key needed) ---
    if state.upper() == "WA":
        try:
            import xml.etree.ElementTree as _ET

            _wa_url = (
                "https://wslwebservices.leg.wa.gov/LegislationService.asmx/"
                "GetLegislativeStatusChangesByDateRange"
                "?biennium=2025-26"
                "&beginDate=2025-01-01&endDate=2026-12-31"
            )
            req = urllib.request.Request(
                _wa_url,
                headers={
                    "User-Agent": "Familiar-Agent/1.15 (nonprofit-tool)",
                },
            )
            with urllib.request.urlopen(req, timeout=15) as resp:
                xml_data = resp.read()
            root = _ET.fromstring(xml_data)
            ns = {"ws": "http://WSLWebServices.leg.wa.gov/"}
            changes = root.findall(".//ws:LegislativeStatus", ns)

            # Filter by keyword, deduplicate by bill ID, take most recent action
            q_lower = query.lower()
            bill_map = {}
            for c in changes:
                bid_el = c.find("ws:BillId", ns)
                action_el = c.find("ws:HistoryLine", ns)
                date_el = c.find("ws:ActionDate", ns)
                bid = bid_el.text if bid_el is not None else ""
                action = action_el.text if action_el is not None else ""
                adate = (date_el.text or "")[:10] if date_el is not None else ""

                # Match keyword against bill ID or action text
                if q_lower not in bid.lower() and q_lower not in action.lower():
                    continue

                if bid not in bill_map or adate > bill_map[bid]["latest_action_date"]:
                    bill_map[bid] = {
                        "bill_id": bid,
                        "title": bid,  # WA API doesn't include title in status changes
                        "chamber": "House" if bid.startswith("H") else "Senate",
                        "congress": "2025-26",
                        "state": "WA",
                        "level": "state",
                        "latest_action": action,
                        "latest_action_date": adate,
                        "url": f"https://app.leg.wa.gov/billsummary?BillNumber="
                        f"{bid.replace('HB ', '').replace('SB ', '').replace('SHB ', '').replace('SSB ', '').split()[0]}"
                        f"&Year=2025",
                        "sponsor": "",
                        "update_date": adate,
                    }

            bills = sorted(bill_map.values(), key=lambda b: b["latest_action_date"], reverse=True)
            total = len(bills)
            start = (page - 1) * limit
            page_bills = bills[start : start + limit]

            return jsonify(
                {
                    "bills": page_bills,
                    "total": total,
                    "page": page,
                    "limit": limit,
                    "has_more": start + limit < total,
                    "source": "WA Legislature (leg.wa.gov)",
                }
            )
        except Exception as e:
            logger.warning("WA Legislature search failed: %s", e)

    # Neither configured and no built-in fallback for this state
    return jsonify(
        {
            "error": (
                "State legislation search requires an API key for most states. "
                "Set OPENSTATES_API_KEY (free at openstates.org/accounts/signup) "
                "or LEGISCAN_API_KEY (free at legiscan.com/legiscan) "
                "in Settings > Credentials. "
                "Washington State (WA) and Seattle work without keys."
            ),
            "setup_hint": True,
        }
    ), 501


@bp.route("/api/legislation/search/city")
@require_auth
def api_legislation_search_city():
    """Search city council legislation via Legistar API.

    Currently supports Seattle. Extensible to any city using Legistar
    (NYC, Chicago, LA, Portland, etc.) by adding to LEGISTAR_CITIES.
    """
    query = request.args.get("query", "")
    city = request.args.get("city", "seattle").lower()
    limit = min(int(request.args.get("limit", "25")), 50)
    page = int(request.args.get("page", "1"))

    if not query:
        return jsonify({"error": "Provide a search query"}), 400

    # Cities that use Legistar (free, no key needed)
    LEGISTAR_CITIES = {
        "seattle": {"client": "seattle", "label": "Seattle City Council"},
        "nyc": {"client": "nyc", "label": "New York City Council"},
        "chicago": {"client": "chicago", "label": "Chicago City Council"},
        "la": {"client": "lacity", "label": "Los Angeles City Council"},
        "portland": {"client": "portland", "label": "Portland City Council"},
        "denver": {"client": "denver", "label": "Denver City Council"},
        "philadelphia": {"client": "phlapi", "label": "Philadelphia City Council"},
        "san_francisco": {"client": "sfgov", "label": "San Francisco Board of Supervisors"},
    }

    city_cfg = LEGISTAR_CITIES.get(city)
    if not city_cfg:
        return jsonify(
            {
                "error": f"City '{city}' not supported yet. Available: {', '.join(LEGISTAR_CITIES.keys())}",
                "available_cities": list(LEGISTAR_CITIES.keys()),
            }
        ), 400

    try:
        import urllib.parse
        import urllib.request

        skip = (page - 1) * limit
        # Legistar OData filter — search title text
        odata_filter = urllib.parse.quote(f"substringof('{query}', MatterTitle) eq true")
        url = (
            f"https://webapi.legistar.com/v1/{city_cfg['client']}/matters"
            f"?$top={limit}&$skip={skip}"
            f"&$filter={odata_filter}"
            f"&$orderby=MatterLastModifiedUtc+desc"
        )
        req = urllib.request.Request(
            url,
            headers={
                "Accept": "application/json",
                "User-Agent": "Familiar-Agent/1.15 (nonprofit-tool)",
            },
        )
        with urllib.request.urlopen(req, timeout=15) as resp:
            matters = json.loads(resp.read())

        bills = []
        for m in matters or []:
            title = m.get("MatterTitle", "") or m.get("MatterName", "")
            bills.append(
                {
                    "bill_id": m.get("MatterFile", ""),
                    "title": title[:200],
                    "chamber": m.get("MatterBodyName", ""),
                    "congress": "",
                    "state": "",
                    "level": "city",
                    "city": city,
                    "latest_action": m.get("MatterStatusName", ""),
                    "latest_action_date": (m.get("MatterIntroDate") or "")[:10],
                    "url": (
                        f"https://{city_cfg['client']}.legistar.com/"
                        f"LegislationDetail.aspx?ID={m.get('MatterId', '')}"
                        f"&GUID={m.get('MatterGuid', '')}"
                    ),
                    "sponsor": "",
                    "type": m.get("MatterTypeName", ""),
                    "update_date": (m.get("MatterLastModifiedUtc") or "")[:10],
                }
            )

        # Legistar doesn't return total count easily — estimate from results
        has_more = len(bills) == limit
        total = skip + len(bills) + (1 if has_more else 0)

        return jsonify(
            {
                "bills": bills,
                "total": total,
                "page": page,
                "limit": limit,
                "has_more": has_more,
                "source": city_cfg["label"] + " (Legistar)",
            }
        )
    except Exception as e:
        return jsonify({"error": safe_error(e)}), 502


@bp.route("/api/nonprofit/legislation")
@require_auth
def api_legislation_list():
    """List all tracked legislation (uses advocacy BillStore)."""
    try:
        from familiar.skills.advocacy.skill import get_bill_store

        store = get_bill_store()
        return jsonify({"legislation": store.list_bills()})
    except Exception as e:
        return jsonify({"legislation": [], "error": safe_error(e)})


@bp.route("/api/nonprofit/legislation", methods=["POST"])
@require_auth
def api_legislation_add():
    """Track a bill via advocacy BillStore."""
    data = request.json or {}
    if not data.get("title", "").strip():
        return jsonify({"error": "Bill title is required."}), 400
    try:
        from familiar.skills.advocacy.skill import get_bill_store

        store = get_bill_store()
        bill_id = store.track_bill(
            bill_number=data.get("bill_id", ""),
            title=data.get("title", ""),
            jurisdiction="federal" if data.get("level") == "federal" else data.get("state", ""),
            session=data.get("congress", ""),
            status="introduced",
            our_position="watch",
            priority=data.get("priority", "medium"),
            sponsor=data.get("sponsor", ""),
            summary=data.get("latest_action", ""),
        )
        if bill_id is None:
            return jsonify({"error": "Bill already tracked."}), 409
        bill = store.get_bill(bill_id)
        return jsonify({"bill": bill}), 201
    except Exception as e:
        return jsonify({"error": safe_error(e)}), 500


@bp.route("/api/nonprofit/legislation/<bill_id>", methods=["DELETE"])
@require_auth
def api_legislation_delete(bill_id):
    """Stop tracking a bill."""
    try:
        from familiar.skills.advocacy.skill import get_bill_store

        store = get_bill_store()
        ok = store.delete_bill(bill_id)
        return jsonify({"deleted": ok}), 200 if ok else 404
    except Exception as e:
        return jsonify({"error": safe_error(e)}), 500


@bp.route("/api/nonprofit/watchlist")
@require_auth
def api_watchlist_list():
    """Get bill keyword watchlist."""
    from familiar.skills.nonprofit.skill import get_bill_watchlist

    return jsonify({"watchlist": get_bill_watchlist()})


@bp.route("/api/nonprofit/watchlist", methods=["POST"])
@require_auth
def api_watchlist_add():
    """Add keyword to watchlist."""
    data = request.json or {}
    if not data.get("keyword", "").strip():
        return jsonify({"error": "Keyword is required."}), 400
    from familiar.skills.nonprofit.skill import add_watchlist_keyword

    msg = add_watchlist_keyword(data)
    from familiar.skills.nonprofit.skill import get_bill_watchlist

    return jsonify({"message": msg, "watchlist": get_bill_watchlist()}), 201


@bp.route("/api/nonprofit/watchlist/<keyword_id>", methods=["DELETE"])
@require_auth
def api_watchlist_delete(keyword_id):
    """Remove keyword from watchlist."""
    from familiar.skills.nonprofit.skill import remove_watchlist_keyword

    ok = remove_watchlist_keyword(keyword_id)
    return jsonify({"deleted": ok}), 200 if ok else 404



# ============================================================
# CSV Export Endpoints
# ============================================================


@bp.route("/api/nonprofit/donors/export")
@require_auth
def api_nonprofit_donors_export():
    """Export donors as CSV file download."""
    import csv
    import io

    from familiar.skills.nonprofit.skill import get_all_donors

    donors = get_all_donors()
    output = io.StringIO()
    writer = csv.writer(output)
    writer.writerow(
        [
            "name",
            "email",
            "phone",
            "total_given",
            "first_gift",
            "last_gift",
            "tags",
            "notes",
            "interactions_count",
        ]
    )
    for d in donors:
        writer.writerow(
            [
                d.get("name", ""),
                d.get("email", ""),
                d.get("phone", ""),
                d.get("total_given", 0),
                (d.get("first_gift") or "")[:10],
                (d.get("last_gift") or "")[:10],
                ", ".join(d.get("tags", [])),
                d.get("notes", ""),
                len(d.get("interactions", [])),
            ]
        )

    resp = make_response(output.getvalue())
    resp.headers["Content-Disposition"] = "attachment; filename=donors.csv"
    resp.headers["Content-Type"] = "text/csv; charset=utf-8"
    return resp


@bp.route("/api/nonprofit/grants/export")
@require_auth
def api_nonprofit_grants_export():
    """Export grants as CSV file download."""
    import csv
    import io

    from familiar.skills.nonprofit.skill import get_all_grants

    grants = get_all_grants()
    output = io.StringIO()
    writer = csv.writer(output)
    writer.writerow(
        [
            "name",
            "funder",
            "amount",
            "status",
            "deadline",
            "report_due",
            "start_date",
            "end_date",
            "notes",
        ]
    )
    for g in grants:
        writer.writerow(
            [
                g.get("name", ""),
                g.get("funder", ""),
                g.get("amount", ""),
                g.get("status", ""),
                g.get("deadline", ""),
                g.get("report_due", ""),
                g.get("start_date", ""),
                g.get("end_date", ""),
                g.get("notes", ""),
            ]
        )

    resp = make_response(output.getvalue())
    resp.headers["Content-Disposition"] = "attachment; filename=grants.csv"
    resp.headers["Content-Type"] = "text/csv; charset=utf-8"
    return resp


@bp.route("/api/bookkeeping/transactions/export")
@require_auth
def api_bookkeeping_transactions_export():
    """Export transactions as CSV file download."""
    import csv
    import io

    from familiar.skills.bookkeeping.skill import get_all_transactions

    filters = {
        "start_date": request.args.get("start_date"),
        "end_date": request.args.get("end_date"),
        "category": request.args.get("category"),
        "fund": request.args.get("fund"),
        "type": request.args.get("type"),
    }
    filters = {k: v for k, v in filters.items() if v}
    txns = get_all_transactions(filters if filters else None)

    output = io.StringIO()
    writer = csv.writer(output)
    writer.writerow(
        ["date", "type", "amount", "category", "fund", "source", "vendor", "description", "id"]
    )
    for t in txns:
        writer.writerow(
            [
                t.get("date", ""),
                t.get("type", ""),
                t.get("amount", ""),
                t.get("category", ""),
                t.get("fund", ""),
                t.get("source", ""),
                t.get("vendor", ""),
                t.get("description", ""),
                t.get("id", ""),
            ]
        )

    resp = make_response(output.getvalue())
    resp.headers["Content-Disposition"] = "attachment; filename=transactions.csv"
    resp.headers["Content-Type"] = "text/csv; charset=utf-8"
    return resp


# ============================================================
# Document Generation Endpoints
# ============================================================


@bp.route("/api/nonprofit/generate/receipt", methods=["POST"])
@require_auth
def api_nonprofit_generate_receipt():
    """Generate a tax-deductible donation receipt for a donor."""
    from familiar.skills.documents.skill import create_receipt

    data = request.get_json(force=True)
    donor_name = data.get("donor_name", "Donor")
    amount = data.get("amount", 0)
    date_of_gift = data.get("date", "")
    description = data.get("description", "Charitable donation")

    result = create_receipt(
        {
            "donor_name": donor_name,
            "amount": amount,
            "date_of_gift": date_of_gift,
            "description": description,
            "format": "docx",
        }
    )
    # result is a string like "Receipt saved to /path/to/file.docx"
    # Extract path and send as download
    return _send_generated_doc(result)


@bp.route("/api/nonprofit/generate/thankyou", methods=["POST"])
@require_auth
def api_nonprofit_generate_thankyou():
    """Generate a thank-you letter for a donor using the bundled template."""
    from familiar.skills.documents.skill import fill_template

    data = request.get_json(force=True)
    fields = {
        "donor_name": data.get("donor_name", "Valued Donor"),
        "amount": str(data.get("amount", "")),
        "organization": data.get("organization", "Our Organization"),
        "gift_date": data.get("gift_date", ""),
        "impact_area": data.get("impact_area", "our mission"),
        "personal_note": data.get("personal_note", ""),
        "mission_statement": data.get("mission_statement", ""),
        "signer_name": data.get("signer_name", ""),
        "signer_title": data.get("signer_title", "Executive Director"),
    }

    result = fill_template(
        {
            "template": "nonprofit_director_donor_thankyou",
            "fields": fields,
            "format": "docx",
        }
    )
    return _send_generated_doc(result)


@bp.route("/api/nonprofit/generate/board-report", methods=["POST"])
@require_auth
def api_nonprofit_generate_board_report():
    """Generate a board report with auto-populated financial data."""
    from datetime import datetime as _dt

    from familiar.skills.bookkeeping.skill import get_all_transactions
    from familiar.skills.documents.skill import fill_template
    from familiar.skills.nonprofit.skill import get_all_donors, get_all_grants

    now = _dt.now()
    year = str(now.year)
    txns = get_all_transactions({"start_date": f"{year}-01-01", "end_date": f"{year}-12-31"})

    # Compute financials
    total_income = sum(t["amount"] for t in txns if t["type"] == "income")
    total_expense = sum(t["amount"] for t in txns if t["type"] == "expense")
    net = total_income - total_expense

    donors = get_all_donors()
    grants = get_all_grants()
    active_grants = [g for g in grants if g.get("status") in ("applied", "awarded")]

    # Build sections text
    fin_overview = (
        f"Year-to-date income: ${total_income:,.2f}\n"
        f"Year-to-date expenses: ${total_expense:,.2f}\n"
        f"Net position: ${net:,.2f}\n"
        f"Total transactions: {len(txns)}"
    )

    fundraising = (
        f"Total donors: {len(donors)}\n"
        f"Total donated (all time): ${sum(d.get('total_given', 0) for d in donors):,.2f}"
    )

    grant_lines = []
    for g in active_grants:
        status = g.get("status", "unknown").title()
        amt = g.get("amount", 0)
        grant_lines.append(f"- {g.get('name', 'Unnamed')}: {status} (${amt:,.0f})")
    grant_text = "\n".join(grant_lines) if grant_lines else "No active grants."

    data = request.get_json(force=True) or {}
    report_period = data.get("report_period", now.strftime("%B %Y"))

    fields = {
        "report_period": report_period,
        "executive_summary": data.get(
            "executive_summary",
            f"Financial and program summary for {report_period}.",
        ),
        "financial_overview": fin_overview,
        "program_updates": data.get("program_updates", "See grant pipeline below."),
        "fundraising_report": fundraising + "\n\nGrant Pipeline:\n" + grant_text,
        "upcoming_events": data.get("upcoming_events", "None scheduled."),
        "action_items": data.get("action_items", "None."),
        "motions": data.get("motions", "None."),
    }

    result = fill_template(
        {
            "template": "nonprofit_director_board_report",
            "fields": fields,
            "format": "docx",
        }
    )
    return _send_generated_doc(result)


@bp.route("/api/nonprofit/generate/990-data", methods=["POST"])
@require_auth
def api_nonprofit_generate_990_data():
    """Generate 990 filing data export as a structured report."""
    from datetime import datetime as _dt

    from familiar.skills.bookkeeping.skill import get_all_transactions
    from familiar.skills.documents.skill import create_report
    from familiar.skills.nonprofit.skill import get_all_donors, get_all_grants

    data = request.get_json(force=True) or {}
    year = data.get("year", str(_dt.now().year))
    txns = get_all_transactions({"start_date": f"{year}-01-01", "end_date": f"{year}-12-31"})

    income_txns = [t for t in txns if t["type"] == "income"]
    expense_txns = [t for t in txns if t["type"] == "expense"]
    total_income = sum(t["amount"] for t in income_txns)
    total_expense = sum(t["amount"] for t in expense_txns)

    # Income by category
    income_cats = {}
    for t in income_txns:
        cat = t.get("category", "other").replace("_", " ").title()
        income_cats[cat] = income_cats.get(cat, 0) + t["amount"]

    # Expense by category
    expense_cats = {}
    for t in expense_txns:
        cat = t.get("category", "other").replace("_", " ").title()
        expense_cats[cat] = expense_cats.get(cat, 0) + t["amount"]

    donors = get_all_donors()
    grants = get_all_grants()
    awarded = [g for g in grants if g.get("status") == "awarded"]

    sections = [
        {
            "heading": "Revenue Summary",
            "body": f"Total Revenue: ${total_income:,.2f}",
            "table": {
                "headers": ["Category", "Amount"],
                "rows": [
                    [k, f"${v:,.2f}"]
                    for k, v in sorted(income_cats.items(), key=lambda x: x[1], reverse=True)
                ],
            },
        },
        {
            "heading": "Expense Summary",
            "body": f"Total Expenses: ${total_expense:,.2f}",
            "table": {
                "headers": ["Category", "Amount"],
                "rows": [
                    [k, f"${v:,.2f}"]
                    for k, v in sorted(expense_cats.items(), key=lambda x: x[1], reverse=True)
                ],
            },
        },
        {
            "heading": "Net Assets",
            "body": (
                f"Net Income: ${total_income - total_expense:,.2f}\nTotal transactions: {len(txns)}"
            ),
        },
        {
            "heading": "Contributor Information",
            "body": (
                f"Total individual contributors: {len(donors)}\n"
                f"Total individual contributions: "
                f"${sum(d.get('total_given', 0) for d in donors):,.2f}\n"
                f"Active grants received: {len(awarded)}\n"
                f"Grant revenue: "
                f"${sum(g.get('amount', 0) for g in awarded):,.2f}"
            ),
        },
    ]

    result = create_report(
        {
            "title": f"Form 990 Data — Fiscal Year {year}",
            "sections": sections,
            "format": "docx",
            "letterhead": True,
        }
    )
    return _send_generated_doc(result)


# ============================================================
# Nonprofit Email Pipeline API — Pending Events + Action Queue
# ============================================================


@bp.route("/api/nonprofit/pending-events")
@require_auth
def api_nonprofit_pending_events():
    """List all pending calendar events detected from emails."""
    from familiar.skills.email.pending_events import get_pending_event_store

    store = get_pending_event_store()
    return jsonify({"events": store.get_pending(), "count": store.count_pending()})


@bp.route("/api/nonprofit/pending-events/<event_id>/confirm", methods=["POST"])
@require_auth
def api_nonprofit_pending_event_confirm(event_id):
    """Confirm a pending event (add to calendar)."""
    from familiar.skills.email.pending_events import get_pending_event_store

    store = get_pending_event_store()
    if store.mark_confirmed(event_id):
        return jsonify({"status": "confirmed", "id": event_id})
    return jsonify({"error": "Event not found."}), 404


@bp.route("/api/nonprofit/pending-events/<event_id>/dismiss", methods=["POST"])
@require_auth
def api_nonprofit_pending_event_dismiss(event_id):
    """Dismiss a pending event (don't add to calendar)."""
    from familiar.skills.email.pending_events import get_pending_event_store

    store = get_pending_event_store()
    if store.mark_dismissed(event_id):
        return jsonify({"status": "dismissed", "id": event_id})
    return jsonify({"error": "Event not found."}), 404


@bp.route("/api/nonprofit/action-queue")
@require_auth
def api_nonprofit_action_queue():
    """List all pending email follow-up actions."""
    from familiar.skills.email.action_queue import get_action_store

    store = get_action_store()
    return jsonify(
        {
            "actions": store.get_pending(),
            "count": store.count_pending(),
            "by_type": store.summary_by_type(),
        }
    )


@bp.route("/api/nonprofit/action-queue/<action_id>/complete", methods=["POST"])
@require_auth
def api_nonprofit_action_complete(action_id):
    """Mark an action as completed."""
    from familiar.skills.email.action_queue import get_action_store

    store = get_action_store()
    if store.mark_complete(action_id):
        return jsonify({"status": "completed", "id": action_id})
    return jsonify({"error": "Action not found."}), 404


@bp.route("/api/nonprofit/action-queue/<action_id>/delegate", methods=["POST"])
@require_auth
def api_nonprofit_action_delegate(action_id):
    """Delegate an action to a staff member."""
    data = request.json or {}
    delegate_to = data.get("delegate_to", "").strip()
    if not delegate_to:
        return jsonify({"error": "delegate_to is required."}), 400

    from familiar.skills.email.action_queue import get_action_store

    store = get_action_store()
    if store.mark_delegated(action_id, delegate_to):
        return jsonify({"status": "delegated", "id": action_id, "delegate_to": delegate_to})
    return jsonify({"error": "Action not found."}), 404


# ============================================================
# Event Management API
# ============================================================


@bp.route("/api/nonprofit/events")
@require_auth
def api_nonprofit_events():
    """List all events."""
    from familiar.skills.events.skill import get_event_store

    store = get_event_store()
    status = request.args.get("status")
    return jsonify({"events": store.list_events(status=status)})


@bp.route("/api/nonprofit/events", methods=["POST"])
@require_auth
def api_nonprofit_event_create():
    """Create a new event."""
    data = request.json or {}
    if not data.get("name", "").strip():
        return jsonify({"error": "Event name is required."}), 400
    from familiar.skills.events.skill import get_event_store

    store = get_event_store()
    eid = store.create_event(
        name=data["name"].strip(),
        event_type=data.get("event_type", "meeting"),
        start_date=data.get("start_date", ""),
        end_date=data.get("end_date", ""),
        budget_total=float(data.get("budget_total", 0)),
        description=data.get("description", ""),
        volunteer_slots_total=int(data.get("volunteer_slots_total", 0)),
        venue=data.get("venue"),
        partner_orgs=data.get("partner_orgs"),
    )
    return jsonify({"event": store.get_event(eid)}), 201


@bp.route("/api/nonprofit/events/<event_id>", methods=["PUT"])
@require_auth
def api_nonprofit_event_update(event_id):
    """Update an event."""
    data = request.json or {}
    from familiar.skills.events.skill import get_event_store

    store = get_event_store()
    result = store.update_event(event_id, data)
    if result:
        return jsonify({"event": result})
    return jsonify({"error": "Event not found."}), 404


@bp.route("/api/nonprofit/events/<event_id>", methods=["DELETE"])
@require_auth
def api_nonprofit_event_delete(event_id):
    """Delete an event."""
    from familiar.skills.events.skill import get_event_store

    store = get_event_store()
    if store.delete_event(event_id):
        return jsonify({"status": "deleted"})
    return jsonify({"error": "Event not found."}), 404


@bp.route("/api/nonprofit/events/<event_id>/vendors")
@require_auth
def api_nonprofit_event_vendors(event_id):
    """List vendors for an event."""
    from familiar.skills.events.skill import get_vendor_store

    return jsonify({"vendors": get_vendor_store().list_vendors(event_id=event_id)})


@bp.route("/api/nonprofit/events/<event_id>/vendors", methods=["POST"])
@require_auth
def api_nonprofit_event_vendor_add(event_id):
    """Add a vendor to an event."""
    data = request.json or {}
    if not data.get("name", "").strip():
        return jsonify({"error": "Vendor name is required."}), 400
    from familiar.skills.events.skill import get_vendor_store

    store = get_vendor_store()
    vid = store.add_vendor(
        event_id=event_id,
        name=data["name"].strip(),
        service_type=data.get("service_type", "other"),
        contact_name=data.get("contact_name", ""),
        contact_email=data.get("contact_email", ""),
        quote_amount=float(data.get("quote_amount", 0)),
    )
    return jsonify({"vendor_id": vid}), 201


@bp.route("/api/nonprofit/events/<event_id>/volunteers")
@require_auth
def api_nonprofit_event_volunteers(event_id):
    """List volunteers for an event."""
    from familiar.skills.events.skill import get_volunteer_store

    return jsonify({"volunteers": get_volunteer_store().list_volunteers(event_id=event_id)})


@bp.route("/api/nonprofit/events/<event_id>/volunteers", methods=["POST"])
@require_auth
def api_nonprofit_event_volunteer_add(event_id):
    """Add a volunteer to an event."""
    data = request.json or {}
    if not data.get("name", "").strip():
        return jsonify({"error": "Volunteer name is required."}), 400
    from familiar.skills.events.skill import get_volunteer_store

    store = get_volunteer_store()
    vid = store.add_volunteer(
        event_id=event_id,
        name=data["name"].strip(),
        email=data.get("email", ""),
        role=data.get("role", "other"),
        shift_start=data.get("shift_start", ""),
        shift_end=data.get("shift_end", ""),
    )
    return jsonify({"volunteer_id": vid}), 201


# ============================================================
# Advocacy & Legislative API
# ============================================================


@bp.route("/api/nonprofit/bills")
@require_auth
def api_nonprofit_bills():
    """List tracked bills."""
    from familiar.skills.advocacy.skill import get_bill_store

    store = get_bill_store()
    return jsonify(
        {
            "bills": store.list_bills(
                position=request.args.get("position"),
                priority=request.args.get("priority"),
            )
        }
    )


@bp.route("/api/nonprofit/bills", methods=["POST"])
@require_auth
def api_nonprofit_bill_add():
    """Track a new bill."""
    data = request.json or {}
    if not data.get("bill_number", "").strip() or not data.get("title", "").strip():
        return jsonify({"error": "bill_number and title are required."}), 400
    from familiar.skills.advocacy.skill import get_bill_store

    store = get_bill_store()
    bid = store.track_bill(
        bill_number=data["bill_number"].strip(),
        title=data["title"].strip(),
        jurisdiction=data.get("jurisdiction", "WA"),
        status=data.get("status", "introduced"),
        our_position=data.get("our_position", "watch"),
        priority=data.get("priority", "medium"),
        sponsor=data.get("sponsor", ""),
        committee=data.get("committee", ""),
        summary=data.get("summary", ""),
    )
    if bid:
        return jsonify({"bill": store.get_bill(bid)}), 201
    return jsonify({"error": "Bill already tracked."}), 409


@bp.route("/api/nonprofit/bills/<bill_id>", methods=["PUT"])
@require_auth
def api_nonprofit_bill_update(bill_id):
    """Update a tracked bill."""
    data = request.json or {}
    from familiar.skills.advocacy.skill import get_bill_store

    store = get_bill_store()
    result = store.update_bill(bill_id, data)
    if result:
        return jsonify({"bill": result})
    return jsonify({"error": "Bill not found."}), 404


@bp.route("/api/nonprofit/bills/<bill_id>", methods=["DELETE"])
@require_auth
def api_nonprofit_bill_delete(bill_id):
    """Stop tracking a bill."""
    from familiar.skills.advocacy.skill import get_bill_store

    if get_bill_store().remove_bill(bill_id):
        return jsonify({"status": "removed"})
    return jsonify({"error": "Bill not found."}), 404


@bp.route("/api/nonprofit/hearings")
@require_auth
def api_nonprofit_hearings():
    """List upcoming hearings across all bills."""
    days = int(request.args.get("days", 30))
    from familiar.skills.advocacy.skill import get_bill_store

    return jsonify({"hearings": get_bill_store().list_upcoming_hearings(days=days)})


@bp.route("/api/nonprofit/bills/<bill_id>/testimony", methods=["POST"])
@require_auth
def api_nonprofit_bill_testimony(bill_id):
    """Record testimony for a bill."""
    data = request.json or {}
    if not data.get("testifier", "").strip():
        return jsonify({"error": "testifier is required."}), 400
    from familiar.skills.advocacy.skill import get_bill_store

    store = get_bill_store()
    if store.add_testimony(
        bill_id,
        data["testifier"].strip(),
        position=data.get("position", ""),
        format_type=data.get("format", "written"),
    ):
        return jsonify({"status": "recorded"}), 201
    return jsonify({"error": "Bill not found."}), 404


# ============================================================
# Stewardship API
# ============================================================


@bp.route("/api/nonprofit/stewardship")
@require_auth
def api_nonprofit_stewardship():
    """Get stewardship queue — overdue + pending."""
    from familiar.skills.nonprofit.stewardship import get_stewardship_store

    store = get_stewardship_store()
    return jsonify(
        {
            "overdue": store.get_overdue(),
            "pending": store.get_pending(),
            "completion_rate": store.completion_rate(),
        }
    )


@bp.route("/api/nonprofit/stewardship/<entry_id>/complete", methods=["POST"])
@require_auth
def api_nonprofit_stewardship_complete(entry_id):
    """Complete a stewardship action."""
    data = request.json or {}
    index = int(data.get("action_index", 0))
    from familiar.skills.nonprofit.stewardship import get_stewardship_store

    store = get_stewardship_store()
    if store.complete_action(entry_id, index):
        return jsonify({"status": "completed"})
    return jsonify({"error": "Entry or action not found."}), 404


# ============================================================
# Batch Communications API
# ============================================================


@bp.route("/api/nonprofit/batches")
@require_auth
def api_nonprofit_batches():
    """List batch communications."""
    from familiar.skills.nonprofit.batch_comms import get_batch_store

    return jsonify({"batches": get_batch_store().list_batches()})


@bp.route("/api/nonprofit/batches", methods=["POST"])
@require_auth
def api_nonprofit_batch_create():
    """Create a new batch communication."""
    data = request.json or {}
    if not data.get("name", "").strip() and not data.get("segment_key"):
        return jsonify({"error": "name or segment_key is required."}), 400
    from familiar.skills.nonprofit.batch_comms import get_batch_store

    store = get_batch_store()
    bid = store.create_batch(
        name=data.get("name", "").strip(),
        segment_key=data.get("segment_key", ""),
        segment_criteria=data.get("segment_criteria"),
        template_name=data.get("template_name", ""),
        channel=data.get("channel", "email"),
    )
    return jsonify({"batch": store.get_batch(bid)}), 201


@bp.route("/api/nonprofit/batches/<batch_id>")
@require_auth
def api_nonprofit_batch_details(batch_id):
    """Get batch details."""
    from familiar.skills.nonprofit.batch_comms import get_batch_store

    batch = get_batch_store().get_batch(batch_id)
    if batch:
        return jsonify({"batch": batch})
    return jsonify({"error": "Batch not found."}), 404


def _send_generated_doc(result_str):
    """Parse a document skill result string and send the file as download."""
    import re

    # Skill functions return strings like "Receipt saved to /path/to/file.docx"
    # or "Document saved to /path/to/file.docx"
    match = re.search(
        r"(?:saved to|created at|generated at|wrote)\s+(.+\.(?:docx|pdf))",
        result_str,
        re.IGNORECASE,
    )
    if match:
        filepath = match.group(1).strip()
        if os.path.isfile(filepath):
            return send_file(
                filepath,
                as_attachment=True,
                download_name=os.path.basename(filepath),
            )

    # If result looks like an error or no file found, return the raw message
    return jsonify({"message": result_str, "error": "Could not locate generated file."}), 500


