"""property_researcher.py — Reasoning loop que hace research autónomo de properties.

PRINCIPIO:
La IA local (Ollama PropertyResearcher) decide qué tools llamar para investigar
un deal. Loop hasta que llama `finish` o hits max iterations.

USO:
    from property_researcher import research_deal
    result = research_deal(
        deal_id=349,
        max_iterations=15,
        status_cb=print,
    )
    # → {summary, portals_used, documents_saved, findings, iterations, elapsed_seconds}
"""
from __future__ import annotations

import json
import time
import traceback
from typing import Callable, Optional

from agent_tools import OLLAMA_TOOL_SPECS, TOOL_DISPATCH


# ────────────────────────────────────────────────────────────────────────────
# Constants
# ────────────────────────────────────────────────────────────────────────────

MODEL_NAME = "PropertyResearcher"
DEFAULT_MAX_ITERATIONS = 15
TOOL_TIMEOUT_SECONDS = 60  # cap per tool call


# ────────────────────────────────────────────────────────────────────────────
# Main entry point
# ────────────────────────────────────────────────────────────────────────────

def research_deal(
    deal_id: int,
    max_iterations: int = DEFAULT_MAX_ITERATIONS,
    status_cb: Optional[Callable[[str], None]] = None,
) -> dict:
    """Run autonomous research loop on a deal.

    Returns:
        {
          "deal_id": int,
          "summary": str,
          "portals_used": [str],
          "documents_saved": [str],
          "findings": dict,
          "iterations": int,
          "elapsed_seconds": float,
          "errors": [str],
          "finished_cleanly": bool,
          "tool_calls_log": [dict]   # detailed history per iteration
        }
    """
    t0 = time.perf_counter()

    def _log(msg: str) -> None:
        if status_cb:
            status_cb(msg)

    try:
        import ollama
    except ImportError:
        return _error_result(deal_id, "ollama package not installed", t0)

    # Load deal context
    try:
        from deals_db import init_db, get_deal_by_id
        init_db()
        deal = get_deal_by_id(deal_id)
        if not deal:
            return _error_result(deal_id, f"deal_id={deal_id} not found in deals.db", t0)
    except Exception as e:
        return _error_result(deal_id, f"deal load failed: {e}", t0)

    # Build the initial user task description
    task = _build_task_description(deal)
    _log(f"[researcher] Task: {task[:120]}")

    # Conversation history (multi-turn with tool results fed back)
    messages: list[dict] = [
        {"role": "user", "content": task},
    ]

    findings: dict = {}
    portals_used: list[str] = []
    documents_saved: list[str] = []
    errors: list[str] = []
    tool_calls_log: list[dict] = []
    finished_cleanly = False
    final_summary = ""

    for iteration in range(1, max_iterations + 1):
        _log(f"[researcher] iter {iteration}/{max_iterations}: thinking...")

        try:
            response = ollama.chat(
                model=MODEL_NAME,
                messages=messages,
                tools=OLLAMA_TOOL_SPECS,
                options={"temperature": 0.2, "num_ctx": 16384},
            )
        except Exception as e:
            errors.append(f"iter {iteration} ollama.chat failed: {e}")
            _log(f"[researcher]   ERROR: {e}")
            break

        msg = response.get("message", {})
        # Append assistant message to history
        messages.append({"role": "assistant", "content": msg.get("content", ""),
                         "tool_calls": msg.get("tool_calls", [])})

        tool_calls = msg.get("tool_calls", []) or []
        if not tool_calls:
            # No more tools — model decided to finish without calling finish()
            content = msg.get("content", "").strip()
            _log(f"[researcher] no tool_calls; content={content[:200]}")
            if content:
                final_summary = content
            break

        # Execute each tool call sequentially
        for tc in tool_calls:
            fn = tc.get("function", {})
            name = fn.get("name", "")
            args_raw = fn.get("arguments", {})
            args = args_raw if isinstance(args_raw, dict) else _safe_json_loads(args_raw)

            _log(f"[researcher]   → calling {name}({_short_args(args)})")

            # Special-case: finish() ends the loop
            if name == "finish":
                final_summary = args.get("summary", "")
                portals_used = args.get("portals_used", [])
                documents_saved = args.get("documents_saved", [])
                findings = args.get("findings", {})
                finished_cleanly = True
                tool_calls_log.append({"iteration": iteration, "name": name, "args": args})
                _log(f"[researcher]   FINISH: {final_summary[:120]}")
                break

            # Auto-inject deal_id for save/download tools if missing
            if name in ("save_document", "download_pdf") and "deal_id" not in args:
                args["deal_id"] = deal_id

            # Execute the tool
            tool_result = _execute_tool(name, args)
            tool_calls_log.append({
                "iteration": iteration, "name": name,
                "args": _short_args(args, max_len=300),
                "result": _short_result(tool_result),
            })

            # Track outputs of interest
            if name == "save_document" or name == "download_pdf":
                if "saved_to" in tool_result:
                    documents_saved.append(tool_result["saved_to"])
            elif name == "remember_portal":
                if tool_result.get("ok"):
                    portals_used.append(tool_result.get("url", ""))

            # Feed tool result back to the model
            tool_result_str = json.dumps(tool_result, default=str)[:3000]
            messages.append({"role": "tool", "content": tool_result_str, "name": name})

        if finished_cleanly:
            break

    elapsed = time.perf_counter() - t0
    return {
        "deal_id": deal_id,
        "summary": final_summary or "Research completed without explicit summary",
        "portals_used": portals_used,
        "documents_saved": documents_saved,
        "findings": findings,
        "iterations": iteration,
        "elapsed_seconds": round(elapsed, 1),
        "errors": errors,
        "finished_cleanly": finished_cleanly,
        "tool_calls_log": tool_calls_log,
    }


# ────────────────────────────────────────────────────────────────────────────
# Helpers
# ────────────────────────────────────────────────────────────────────────────

def _build_task_description(deal: dict) -> str:
    """Build the initial user prompt describing what to research."""
    parts = [
        f"Research this property deal:",
        f"- deal_id: {deal['id']}",
        f"- Address: {deal.get('address') or '?'}",
        f"- County: {deal.get('county') or '?'}",
        f"- State: {deal.get('state') or '?'}",
        f"- Deal type: {deal.get('deal_type') or '?'}",
    ]
    if deal.get("case_number"):
        parts.append(f"- Case #: {deal['case_number']}")
    if deal.get("parcel_id"):
        parts.append(f"- Parcel ID: {deal['parcel_id']}")
    if deal.get("listing_price"):
        parts.append(f"- Listing price: ${deal['listing_price']:,.0f}")

    parts.append("")
    parts.append("Goal: find and document the 3 BASIC items for pre-screening:")
    parts.append("  1. Property Appraiser data (owner, assessed value, year built, sqft)")
    parts.append("  2. Court records (plaintiff/defendant if foreclosure, or owner if MLS)")
    parts.append("  3. Property photo (if available from PA or other free source)")
    parts.append("")
    parts.append("Save what you find via save_document. Call remember_portal for working portals.")
    parts.append("When done, call finish() with a summary.")
    return "\n".join(parts)


def _execute_tool(name: str, args: dict) -> dict:
    """Dispatch to the actual Python function."""
    fn = TOOL_DISPATCH.get(name)
    if not fn:
        return {"error": f"unknown tool: {name}"}
    try:
        # Filter args to those the function accepts (defensive)
        return fn(**args)
    except TypeError as e:
        return {"error": f"bad args for {name}: {e}"}
    except Exception as e:
        return {"error": f"{name} crashed: {type(e).__name__}: {e}",
                "trace": traceback.format_exc()[:500]}


def _safe_json_loads(s) -> dict:
    if not s:
        return {}
    if isinstance(s, dict):
        return s
    try:
        return json.loads(s)
    except Exception:
        return {}


def _short_args(args: dict, max_len: int = 100) -> str:
    s = json.dumps(args, default=str)
    return s[:max_len] + ("..." if len(s) > max_len else "")


def _short_result(result: dict) -> dict:
    """Truncate large fields so logs don't explode."""
    short = {}
    for k, v in result.items():
        if isinstance(v, str) and len(v) > 200:
            short[k] = v[:200] + f"...({len(v)} chars)"
        elif isinstance(v, list) and len(v) > 5:
            short[k] = v[:5] + [f"...({len(v)} items)"]
        else:
            short[k] = v
    return short


def _error_result(deal_id: int, msg: str, t0: float) -> dict:
    return {
        "deal_id": deal_id,
        "summary": "",
        "portals_used": [],
        "documents_saved": [],
        "findings": {},
        "iterations": 0,
        "elapsed_seconds": round(time.perf_counter() - t0, 1),
        "errors": [msg],
        "finished_cleanly": False,
        "tool_calls_log": [],
    }
