"""
Local stdio MCP server for Appraisal Forge.

Mirrors the 15 tools from the hosted MCP server but runs locally,
connecting to the remote API via HTTPS with the user's Bearer token.

Usage (Claude CLI config in ~/.claude/mcp.json):
    {
        "mcpServers": {
            "appraisal-forge": {
                "command": "appraisal-forge",
                "args": ["mcp-serve"],
                "env": {
                    "APPRAISAL_FORGE_URL": "https://appraisal-forge.com",
                    "APPRAISAL_FORGE_TOKEN": "your-token"
                }
            }
        }
    }
"""
from __future__ import annotations

import asyncio
import logging
import os
import sys
from typing import Any

import mcp.types as types
from mcp.server.lowlevel.server import Server
from mcp.server.stdio import stdio_server

from .client import AppraisalForgeClient, ApiError

logging.basicConfig(level=logging.INFO, stream=sys.stderr)
logger = logging.getLogger("appraisal-forge-mcp")

# Lazy-init client (created on first tool call)
_client: AppraisalForgeClient | None = None


def _get_client() -> AppraisalForgeClient:
    global _client
    if _client is not None:
        return _client

    url = os.environ.get("APPRAISAL_FORGE_URL", "").strip()
    token = os.environ.get("APPRAISAL_FORGE_TOKEN", "").strip()

    if not url:
        raise ValueError(
            "APPRAISAL_FORGE_URL not set. "
            "Add it to the env block in your MCP server config."
        )
    if not token:
        raise ValueError(
            "APPRAISAL_FORGE_TOKEN not set. "
            "Get a token from Settings > API Tokens in Appraisal Forge."
        )

    _client = AppraisalForgeClient(api_url=url, token=token)
    logger.info("Connected to %s", url)
    return _client


# ── MCP Server ──

server = Server("AppraisalForge")


@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="list_appraisals",
            description="List all appraisals with their ID, name, address, and status.",
            inputSchema={"type": "object", "properties": {}},
        ),
        types.Tool(
            name="create_appraisal",
            description="Create a new appraisal. Returns the new appraisal ID.",
            inputSchema={
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string",
                        "description": "Name/description for the appraisal",
                    },
                    "subject_address": {
                        "type": "string",
                        "description": "Subject property address",
                        "default": "",
                    },
                },
                "required": ["name"],
            },
        ),
        types.Tool(
            name="list_sections",
            description="List all form sections with field counts.",
            inputSchema={"type": "object", "properties": {}},
        ),
        types.Tool(
            name="search_schema",
            description="Search field definitions by keyword. Returns field IDs, labels, sections, and types.",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Keyword to search (e.g. 'bath', 'garage', 'lot size')",
                    },
                    "section": {
                        "type": "string",
                        "description": "Filter by section name (e.g. 'Site')",
                        "default": "",
                    },
                    "prefix": {
                        "type": "string",
                        "description": "Filter by field ID prefix (e.g. '0100')",
                        "default": "",
                    },
                    "per_page": {
                        "type": "integer",
                        "description": "Max results to return",
                        "default": 20,
                    },
                },
                "required": ["query"],
            },
        ),
        types.Tool(
            name="get_field_info",
            description="Get detailed metadata for a single field ID.",
            inputSchema={
                "type": "object",
                "properties": {
                    "field_id": {
                        "type": "string",
                        "description": "The UAD field ID (e.g. '0100.0007')",
                    },
                },
                "required": ["field_id"],
            },
        ),
        types.Tool(
            name="get_enumerations",
            description="Get valid dropdown values for a field.",
            inputSchema={
                "type": "object",
                "properties": {
                    "field_id": {
                        "type": "string",
                        "description": "The UAD field ID (e.g. '0700.0117')",
                    },
                },
                "required": ["field_id"],
            },
        ),
        types.Tool(
            name="read_fields",
            description="Read field values from an appraisal.",
            inputSchema={
                "type": "object",
                "properties": {
                    "appraisal_id": {
                        "type": "integer",
                        "description": "The appraisal ID",
                    },
                    "prefix": {
                        "type": "string",
                        "description": "Filter by field ID prefix (e.g. '0100')",
                        "default": "",
                    },
                    "section": {
                        "type": "string",
                        "description": "Filter by section name (e.g. 'Site')",
                        "default": "",
                    },
                    "search": {
                        "type": "string",
                        "description": "Search fields by keyword",
                        "default": "",
                    },
                    "include_empty": {
                        "type": "boolean",
                        "description": "Include fields with no value",
                        "default": False,
                    },
                },
                "required": ["appraisal_id"],
            },
        ),
        types.Tool(
            name="write_fields",
            description='Write field values to an appraisal. Pass a dict of {field_id: value}.',
            inputSchema={
                "type": "object",
                "properties": {
                    "appraisal_id": {
                        "type": "integer",
                        "description": "The appraisal ID",
                    },
                    "fields": {
                        "type": "object",
                        "description": 'Dict mapping field IDs to values, e.g. {"0100.0007": "123 Main St"}',
                        "additionalProperties": {"type": "string"},
                    },
                },
                "required": ["appraisal_id", "fields"],
            },
        ),
        types.Tool(
            name="geocode",
            description="Geocode a street address to lat/lon coordinates.",
            inputSchema={
                "type": "object",
                "properties": {
                    "street": {
                        "type": "string",
                        "description": "Street address (e.g. '123 Main St')",
                    },
                    "city": {"type": "string", "description": "City name", "default": ""},
                    "state": {"type": "string", "description": "State code (e.g. 'CO')", "default": ""},
                    "zip_code": {"type": "string", "description": "ZIP code", "default": ""},
                },
                "required": ["street"],
            },
        ),
        # ── Adjustment Engine Tools ──
        types.Tool(
            name="create_adjustment_session",
            description=(
                "Create a new adjustment session for an appraisal. "
                "Sets up the market-based adjustment calculation engine with "
                "property type, state, effective date, and optional subject overrides."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "appraisal_id": {"type": "integer", "description": "The appraisal ID to attach the session to"},
                    "name": {"type": "string", "description": 'Session name (e.g. "Sales Comparison Adjustments")', "default": ""},
                    "state": {"type": "string", "description": "State code (e.g. 'CO')", "default": ""},
                    "property_type": {"type": "string", "description": "Property type (e.g. 'SFR', 'Condo')", "default": "SFR"},
                    "effective_date": {"type": "string", "description": "Effective date in ISO format (YYYY-MM-DD)", "default": ""},
                    "subject_data": {
                        "type": "object",
                        "description": 'Optional subject property overrides (e.g. {"gla": 2000, "lot_size": 8500})',
                        "default": {},
                    },
                },
                "required": ["appraisal_id"],
            },
        ),
        types.Tool(
            name="upload_comp_data",
            description=(
                "Upload comparable sales data as CSV text to an adjustment session. "
                "The CSV should include columns for address, sale price, GLA, lot size, "
                "and other property features."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "session_id": {"type": "integer", "description": "The adjustment session ID"},
                    "csv_text": {"type": "string", "description": "Raw CSV text with comp data (header row + data rows)"},
                    "dataset_type": {"type": "string", "description": "Type of dataset (e.g. 'comps', 'listings')", "default": "comps"},
                },
                "required": ["session_id", "csv_text"],
            },
        ),
        types.Tool(
            name="run_calculations",
            description=(
                "Run adjustment calculations on a session. Computes market-based "
                "adjustments for each feature using multiple statistical methods "
                "(paired sales, grouped analysis, regression, etc.)."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "session_id": {"type": "integer", "description": "The adjustment session ID"},
                },
                "required": ["session_id"],
            },
        ),
        types.Tool(
            name="set_feature_value",
            description=(
                "Set the chosen adjustment value for a specific feature. "
                "Use after reviewing run_calculations results to confirm or "
                "override the calculated value."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "session_id": {"type": "integer", "description": "The adjustment session ID"},
                    "feature_id": {"type": "integer", "description": "The feature ID to update"},
                    "chosen_value": {"type": "number", "description": "The adjustment value to set (e.g. 50.0 for $50/unit)"},
                    "is_done": {"type": "boolean", "description": "Mark feature as finalized", "default": True},
                },
                "required": ["session_id", "feature_id", "chosen_value"],
            },
        ),
        types.Tool(
            name="apply_adjustments",
            description=(
                "Apply finalized adjustments from a session to the appraisal form. "
                "Chains two API calls: first gets the field values to set, "
                "then writes them to the appraisal."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "session_id": {"type": "integer", "description": "The adjustment session ID"},
                    "appraisal_id": {"type": "integer", "description": "The appraisal ID to write adjustments to"},
                },
                "required": ["session_id", "appraisal_id"],
            },
        ),
        types.Tool(
            name="validate_appraisal",
            description=(
                "Run full validation on an appraisal. Checks UAD 3.6 compliance "
                "(680 rules: required fields, format checks, valid values) and "
                "E&O risk flags (missing comps, large adjustments, stale comps, "
                "value outside range, missing reconciliation, license expiration). "
                "Optionally auto-fixes mechanical issues (address sync, state code "
                "capitalization, whitespace trimming)."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "appraisal_id": {"type": "integer", "description": "The appraisal ID to validate"},
                    "auto_fix": {"type": "boolean", "description": "If true, auto-fix mechanical issues before reporting", "default": False},
                },
                "required": ["appraisal_id"],
            },
        ),
    ]


@server.call_tool()
async def handle_call_tool(
    name: str, arguments: dict | None
) -> list[types.TextContent]:
    args = arguments or {}
    try:
        result = await asyncio.to_thread(_dispatch_tool, name, args)
        return [types.TextContent(type="text", text=result)]
    except Exception as e:
        logger.error("Tool %s error: %s", name, e)
        return [types.TextContent(type="text", text=f"Error: {e}")]


def _dispatch_tool(name: str, args: dict[str, Any]) -> str:
    """Synchronous tool dispatcher (runs in thread via asyncio.to_thread)."""
    client = _get_client()

    if name == "list_appraisals":
        data = client.list_appraisals()
        items = data.get("data", data) if isinstance(data.get("data"), list) else data
        if isinstance(items, list):
            lines = []
            for a in items:
                lines.append(
                    f"ID={a['id']}  {a['name']}  "
                    f"addr={a.get('subject_address', '')}  "
                    f"status={a.get('status', '')}"
                )
            return "\n".join(lines) if lines else "No appraisals found."
        # Fallback: API may return list directly
        if isinstance(items, dict) and not items:
            return "No appraisals found."
        return str(items)

    elif name == "create_appraisal":
        result = client.create_appraisal(
            name=args["name"],
            subject_address=args.get("subject_address", ""),
        )
        return f"Created appraisal ID={result['id']} name={result.get('name', '')}"

    elif name == "list_sections":
        data = client.list_sections()
        lines = []
        for s in data.get("sections", []):
            lines.append(f"Section {s['id']:4s}  {s['name']:40s}  ({s['field_count']} fields)")
        return "\n".join(lines)

    elif name == "search_schema":
        data = client.get_schema(
            search=args["query"],
            section=args.get("section", ""),
            prefix=args.get("prefix", ""),
        )
        fields = data.get("fields", [])
        if not fields:
            return f'No fields found matching "{args["query"]}"'
        lines = [f'Found {data["total"]} fields (showing {len(fields)}):']
        for f in fields:
            enum_note = f" [{len(f['enumerations'])} options]" if f.get("enumerations") else ""
            lines.append(
                f"  {f['id']:15s}  {f['label']:40s}  [{f['section']}]  {f['format_type']}{enum_note}"
            )
        return "\n".join(lines)

    elif name == "get_field_info":
        field_id = args["field_id"]
        data = client.get_schema(search=field_id)
        fields = data.get("fields", [])
        match = next((f for f in fields if f["id"] == field_id), None)
        if not match:
            return f"Field {field_id} not found"
        lines = [
            f"Field: {match['id']}",
            f"Label: {match['label']}",
            f"Section: {match['section']}",
            f"Type: {match['format_type']}",
            f"Definition: {match.get('definition', 'N/A')}",
        ]
        if match.get("enumerations"):
            lines.append("Valid values:")
            for e in match["enumerations"]:
                lines.append(f"  - {e['value']} ({e.get('displayText', '')})")
        return "\n".join(lines)

    elif name == "get_enumerations":
        field_id = args["field_id"]
        data = client.get_enumerations(field_id)
        values = data.get("values", [])
        if not values:
            return f"No enumerations for field {field_id}"
        lines = [f"Valid values for {field_id}:"]
        for v in values:
            lines.append(f"  {v['value']:30s}  {v.get('displayText', '')}")
        return "\n".join(lines)

    elif name == "read_fields":
        data = client.read_fields(
            appraisal_id=args["appraisal_id"],
            prefix=args.get("prefix", "") or None,
            section=args.get("section", "") or None,
            search=args.get("search", "") or None,
            include_empty=args.get("include_empty", False) or None,
        )
        fields = data.get("fields", {})
        if not fields:
            return f"No fields found (appraisal {args['appraisal_id']})"
        lines = [f"Appraisal {args['appraisal_id']} — {data['count']} fields:"]
        for fid, info in sorted(fields.items()):
            label = info.get("label", "")
            value = info.get("value", "")
            lines.append(f"  {fid:20s}  {label:35s}  = {value}")
        return "\n".join(lines)

    elif name == "write_fields":
        result = client.write_fields(
            appraisal_id=args["appraisal_id"],
            fields=args["fields"],
        )
        if "error" in result:
            return f"Error: {result['error']}. Blocked fields: {result.get('blocked_fields', [])}"
        return f"Updated {result['updated']} fields on appraisal {args['appraisal_id']} (total: {result['total_fields']})"

    elif name == "geocode":
        data = client.geocode(
            street=args["street"],
            city=args.get("city", ""),
            state=args.get("state", ""),
            zip_code=args.get("zip_code", ""),
        )
        if not data:
            return "Address not found"
        result = data.get("data", [data])[0] if isinstance(data.get("data"), list) else data
        if isinstance(result, list):
            result = result[0] if result else {}
        return f"lat={result.get('lat', 'N/A')}  lon={result.get('lon', 'N/A')}  matched={result.get('display_name', '')}"

    # ── Adjustment Engine Tools ──

    elif name == "create_adjustment_session":
        data = client.create_adjustment_session(
            appraisal_id=args["appraisal_id"],
            name=args.get("name", ""),
            state=args.get("state", ""),
            property_type=args.get("property_type", "SFR"),
            effective_date=args.get("effective_date", ""),
            subject_data=args.get("subject_data"),
        )
        lines = [
            "Session created:",
            f"  ID: {data.get('id')}",
            f"  Status: {data.get('status', 'N/A')}",
        ]
        features = data.get("features", [])
        if features:
            lines.append(f"  Features ({len(features)}):")
            for feat in features:
                lines.append(
                    f"    id={feat.get('id')}  key={feat.get('feature_key', '')}  "
                    f"label={feat.get('label', '')}"
                )
        return "\n".join(lines)

    elif name == "upload_comp_data":
        data = client.upload_comp_data_text(
            session_id=args["session_id"],
            csv_text=args["csv_text"],
            dataset_type=args.get("dataset_type", "comps"),
        )
        return (
            f"Dataset uploaded:\n"
            f"  Dataset ID: {data.get('id')}\n"
            f"  Row count: {data.get('row_count', 'N/A')}\n"
            f"  Sold count: {data.get('sold_count', 'N/A')}"
        )

    elif name == "run_calculations":
        data = client.run_calculations(session_id=args["session_id"])
        lines = [f"Calculation results for session {args['session_id']}:"]
        features = data.get("features", [])
        if not features:
            lines.append("  No features returned.")
        for feat in features:
            fkey = feat.get("feature_key", "unknown")
            chosen = feat.get("chosen_value")
            lines.append(f"\n  Feature: {fkey} (id={feat.get('id')})")
            methods = feat.get("method_results", {})
            if methods:
                for method_name, method_data in methods.items():
                    val = method_data.get("value", "N/A")
                    conf = method_data.get("confidence", "")
                    conf_str = f"  confidence={conf}" if conf else ""
                    lines.append(f"    {method_name}: {val}{conf_str}")
            else:
                lines.append("    No method results.")
            chosen_str = chosen if chosen is not None else "not set"
            lines.append(f"    Chosen value: {chosen_str}")
        return "\n".join(lines)

    elif name == "set_feature_value":
        data = client.set_feature_value(
            session_id=args["session_id"],
            feature_id=args["feature_id"],
            chosen_value=args["chosen_value"],
            is_done=args.get("is_done", True),
        )
        return (
            f"Feature updated:\n"
            f"  Feature ID: {data.get('id')}\n"
            f"  Key: {data.get('feature_key', 'N/A')}\n"
            f"  Chosen value: {data.get('chosen_value')}\n"
            f"  Is done: {data.get('is_done')}"
        )

    elif name == "apply_adjustments":
        # Step 1: Get the fields to set from the adjustment engine
        apply_data = client.get_apply_data(session_id=args["session_id"])
        fields_to_set = apply_data.get("fields_to_set", {})
        if not fields_to_set:
            return "No adjustment fields to apply (session may have no finalized features)."
        # Step 2: Write those fields to the appraisal
        write_result = client.write_fields(
            appraisal_id=args["appraisal_id"],
            fields=fields_to_set,
        )
        lines = [
            f"Adjustments applied to appraisal {args['appraisal_id']}:",
            f"  Fields set: {len(fields_to_set)}",
            f"  Server confirmed: {write_result.get('updated', 'N/A')} updated",
            "",
            "  Adjustment values written:",
        ]
        for fid, val in sorted(fields_to_set.items()):
            lines.append(f"    {fid} = {val}")
        return "\n".join(lines)

    elif name == "validate_appraisal":
        appraisal_id = args["appraisal_id"]
        do_fix = args.get("auto_fix", False)

        if do_fix:
            data = client.auto_fix(appraisal_id)
        else:
            data = client.validate(appraisal_id)

        summary = data.get("summary", data.get("remaining", {}))
        lines = [
            f"=== Validation Report for Appraisal {appraisal_id} ===",
            "",
        ]

        # Show fixes if auto_fix was used
        fixed = data.get("fixed", [])
        if fixed:
            lines.append(f"--- Auto-Fixed ({len(fixed)}) ---")
            for fix in fixed:
                lines.append(
                    f"  [{fix.get('code', '')}] {fix.get('action', '')}  "
                    f"[field: {fix.get('field_id', 'N/A')}]"
                )
            lines.append("")

        lines.append(
            f"Summary: {summary.get('fatal', 0)} fatal, "
            f"{summary.get('warning', 0)} warnings, "
            f"{summary.get('eo_risk', 0)} E&O risk flags"
            + (f" ({summary.get('fixable', 0)} auto-fixable)" if summary.get("fixable") else "")
        )
        lines.append("")

        # Completeness
        comp = data.get("completeness", {})
        if comp:
            lines.append(
                f"Completeness: {comp.get('required_populated', 0)}/"
                f"{comp.get('required_total', 0)} required fields "
                f"({comp.get('percent', 0)}%)  |  "
                f"{comp.get('populated', 0)}/{comp.get('total_fields', 0)} total"
            )
            lines.append("")

        # UAD issues
        uad = data.get("uad_issues", [])
        fatals = [i for i in uad if i["severity"] == "fatal"]
        warnings = [i for i in uad if i["severity"] == "warning"]

        if fatals:
            lines.append(f"--- UAD Fatal Errors ({len(fatals)}) ---")
            for issue in fatals:
                fix_mark = " \u2605 fixable" if issue.get("fixable") else ""
                field_ref = f"  [field: {issue['field_id']}]" if issue.get("field_id") else ""
                lines.append(
                    f"  [{issue.get('code', '')}] {issue.get('message', '')}  "
                    f"({issue.get('section', '')}){field_ref}{fix_mark}"
                )
            lines.append("")

        if warnings:
            lines.append(f"--- UAD Warnings ({len(warnings)}) ---")
            for issue in warnings:
                field_ref = f"  [field: {issue['field_id']}]" if issue.get("field_id") else ""
                lines.append(
                    f"  [{issue.get('code', '')}] {issue.get('message', '')}  "
                    f"({issue.get('section', '')}){field_ref}"
                )
            lines.append("")

        # E&O risk flags
        eo = data.get("eo_issues", [])
        if eo:
            lines.append(f"--- E&O Risk Flags ({len(eo)}) ---")
            for issue in eo:
                fix_mark = " \u2605 fixable" if issue.get("fixable") else ""
                field_ref = f"  [field: {issue['field_id']}]" if issue.get("field_id") else ""
                lines.append(
                    f"  [{issue.get('code', '')}] {issue.get('message', '')}  "
                    f"({issue.get('section', '')}){field_ref}{fix_mark}"
                )
            lines.append("")

        if not fatals and not warnings and not eo:
            lines.append("No issues found. Appraisal passes all checks.")

        return "\n".join(lines)

    else:
        return f"Unknown tool: {name}"


# ── Entry point ──

async def _run_stdio() -> None:
    """Run with stdio transport."""
    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            server.create_initialization_options(),
        )


def run() -> None:
    """Entry point for ``appraisal-forge mcp-serve``."""
    logger.info("Starting local MCP server (stdio transport)")
    asyncio.run(_run_stdio())
