"""Simulation tools."""

from __future__ import annotations

from sqlite3 import OperationalError
from typing import Any, Literal

from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.exceptions import ToolError
from mcp.types import ToolAnnotations

from idfkit_mcp.errors import safe_tool
from idfkit_mcp.models import (
    ExportTimeseriesResult,
    GetResultsSummaryResult,
    ListOutputVariablesResult,
    QueryTimeseriesResult,
    RunSimulationResult,
)
from idfkit_mcp.state import get_state

_READ_ONLY = ToolAnnotations(readOnlyHint=True, destructiveHint=False, idempotentHint=True, openWorldHint=False)
_RUN = ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=False, openWorldHint=True)
_EXPORT = ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=True, openWorldHint=False)

# Valid EnergyPlus reporting frequencies for time series queries.
ReportingFrequency = Literal["Timestep", "Hourly", "Daily", "Monthly", "RunPeriod", "Annual"]


@safe_tool
def run_simulation(
    weather_file: str | None = None,
    design_day: bool = False,
    annual: bool = False,
    energyplus_dir: str | None = None,
    energyplus_version: str | None = None,
    output_directory: str | None = None,
) -> RunSimulationResult:
    """Execute an EnergyPlus simulation on the loaded model.

    Use this to run the simulation after building or modifying a model.

    Args:
        weather_file: Path to EPW weather file. Uses previously downloaded file if None.
        design_day: Run design-day-only simulation.
        annual: Run annual simulation.
        energyplus_dir: Optional explicit EnergyPlus installation directory or executable path.
        energyplus_version: Optional EnergyPlus version filter (e.g. "25.1.0").
        output_directory: Optional explicit output directory for simulation results.
    """
    from pathlib import Path

    from idfkit.simulation.config import find_energyplus
    from idfkit.simulation.runner import simulate

    state = get_state()
    doc = state.require_model()

    epw_path: Path | None = None
    if weather_file is not None:
        epw_path = Path(weather_file)
    elif state.weather_file is not None:
        epw_path = state.weather_file

    if epw_path is None and not design_day:
        raise ToolError(
            "No weather file specified. Provide weather_file or use download_weather_file first, "
            "or set design_day=True."
        )

    config = find_energyplus(path=energyplus_dir, version=energyplus_version)

    output_dir = Path(output_directory) if output_directory is not None else None
    weather = epw_path if epw_path is not None else ""
    result = simulate(
        doc, weather=weather, design_day=design_day, annual=annual, energyplus=config, output_dir=output_dir
    )

    state.simulation_result = result
    state.save_session()

    errors = result.errors

    error_detail: dict[str, Any] = {
        "fatal": errors.fatal_count,
        "severe": errors.severe_count,
        "warnings": errors.warning_count,
    }
    if errors.has_fatal:
        error_detail["fatal_messages"] = [{"message": m.message, "details": list(m.details)} for m in errors.fatal]
    if errors.has_severe:
        error_detail["severe_messages"] = [
            {"message": m.message, "details": list(m.details)} for m in errors.severe[:10]
        ]
    if errors.warning_count > 0:
        error_detail["warning_messages"] = [
            {"message": m.message, "details": list(m.details)} for m in errors.warnings[:10]
        ]

    return RunSimulationResult.model_validate({
        "success": result.success,
        "runtime_seconds": round(result.runtime_seconds, 2),
        "output_directory": str(result.run_dir),
        "energyplus": {
            "version": ".".join(str(part) for part in config.version),
            "install_dir": str(config.install_dir),
            "executable": str(config.executable),
        },
        "errors": error_detail,
        "simulation_complete": errors.simulation_complete,
    })


@safe_tool
def get_results_summary() -> GetResultsSummaryResult:
    """Get a summary of the last simulation results.

    Use this after run_simulation to review energy metrics, error counts, and key tables.
    """
    state = get_state()
    result = state.require_simulation_result()

    summary: dict[str, Any] = {
        "success": result.success,
        "runtime_seconds": round(result.runtime_seconds, 2),
        "output_directory": str(result.run_dir),
    }

    errors = result.errors
    summary["errors"] = {
        "fatal": errors.fatal_count,
        "severe": errors.severe_count,
        "warnings": errors.warning_count,
        "summary": errors.summary(),
    }

    if errors.has_fatal or errors.has_severe:
        severe_msgs = [{"message": m.message, "details": list(m.details)} for m in errors.severe[:10]]
        fatal_msgs = [{"message": m.message, "details": list(m.details)} for m in errors.fatal]
        summary["fatal_messages"] = fatal_msgs
        summary["severe_messages"] = severe_msgs

    html = result.html
    if html is not None:
        tables_summary: list[dict[str, Any]] = []
        for table in html.tables[:20]:
            table_info: dict[str, Any] = {
                "title": table.title,
                "report": table.report_name,
                "for_string": table.for_string,
            }
            table_dict = table.to_dict()
            if table_dict:
                table_info["data"] = table_dict
            tables_summary.append(table_info)
        summary["tables"] = tables_summary

    return GetResultsSummaryResult.model_validate(summary)


@safe_tool
def list_output_variables(search: str | None = None, limit: int = 50) -> ListOutputVariablesResult:
    """List available output variables from the last simulation.

    Use this to discover what time series data is available for querying.

    Args:
        search: Optional regex pattern to filter variables by name.
        limit: Maximum number of results (default 50).
    """
    state = get_state()
    result = state.require_simulation_result()

    variables = result.variables
    if variables is None:
        raise ToolError("No output variable index available. The simulation may not have produced .rdd/.mdd files.")

    from idfkit.simulation.parsers.rdd import OutputVariable

    all_items = variables.search(search) if search else [*variables.variables, *variables.meters]

    limited = all_items[:limit]
    serialized: list[dict[str, str]] = []
    for item in limited:
        entry: dict[str, str] = {"name": item.name, "units": item.units}
        if isinstance(item, OutputVariable):
            entry["key"] = item.key
            entry["type"] = "variable"
        else:
            entry["type"] = "meter"
        serialized.append(entry)

    total = len(variables.variables) + len(variables.meters)
    return ListOutputVariablesResult.model_validate({
        "total_available": total,
        "returned": len(serialized),
        "variables": serialized,
    })


@safe_tool
def query_timeseries(
    variable_name: str,
    key_value: str = "*",
    frequency: ReportingFrequency | None = None,
    environment: Literal["sizing", "annual"] | None = None,
    limit: int = 24,
) -> QueryTimeseriesResult:
    """Query time series data from the last simulation's SQL output.

    Use this for quick inspection of simulation output data inline.
    Returns the first `limit` data points.

    Args:
        variable_name: The output variable name (e.g. "Zone Mean Air Temperature").
        key_value: Key value such as zone or surface name. Use "*" for environment-level variables.
        frequency: Reporting frequency filter (e.g. "Hourly", "Monthly").
        environment: Filter by environment type: "sizing" or "annual".
        limit: Maximum number of data points to return (default 24).
    """
    state = get_state()
    result = state.require_simulation_result()

    sql = result.sql
    if sql is None:
        raise ToolError("No SQL output available. The simulation may not have produced an .sql file.")

    try:
        ts = sql.get_timeseries(
            variable_name=variable_name,
            key_value=key_value,
            frequency=frequency,
            environment=environment,
        )
    except OperationalError as e:
        raise ToolError(
            f"SQL query failed: {e}. "
            "The simulation may not have completed successfully, or Output:SQLite was not configured in the model. "
            "Check run_simulation results for errors."
        ) from e

    rows = [
        {"timestamp": ts.timestamps[i].isoformat(), "value": ts.values[i]} for i in range(min(limit, len(ts.values)))
    ]

    return QueryTimeseriesResult.model_validate({
        "variable_name": ts.variable_name,
        "key_value": ts.key_value,
        "units": ts.units,
        "frequency": ts.frequency,
        "total_points": len(ts.values),
        "returned": len(rows),
        "data": rows,
    })


@safe_tool
def export_timeseries(
    variable_name: str,
    key_value: str = "*",
    frequency: ReportingFrequency | None = None,
    environment: Literal["sizing", "annual"] | None = None,
    output_path: str | None = None,
) -> ExportTimeseriesResult:
    """Export time series data from the last simulation to a CSV file.

    Use this to save full simulation output data for external analysis.

    Args:
        variable_name: The output variable name (e.g. "Zone Mean Air Temperature").
        key_value: Key value such as zone or surface name. Use "*" for environment-level variables.
        frequency: Reporting frequency filter (e.g. "Hourly", "Monthly").
        environment: Filter by environment type: "sizing" or "annual".
        output_path: Output CSV file path. Defaults to a file in the simulation output directory.
    """
    import csv
    import re
    from pathlib import Path

    state = get_state()
    result = state.require_simulation_result()

    sql = result.sql
    if sql is None:
        raise ToolError("No SQL output available. The simulation may not have produced an .sql file.")

    try:
        ts = sql.get_timeseries(
            variable_name=variable_name,
            key_value=key_value,
            frequency=frequency,
            environment=environment,
        )
    except OperationalError as e:
        raise ToolError(
            f"SQL query failed: {e}. "
            "The simulation may not have completed successfully, or Output:SQLite was not configured in the model. "
            "Check run_simulation results for errors."
        ) from e

    if output_path is not None:
        csv_path = Path(output_path)
    else:
        safe_name = re.sub(r"[^\w]+", "_", variable_name).strip("_").lower()
        csv_path = result.run_dir / f"timeseries_{safe_name}.csv"

    with csv_path.open("w", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(["timestamp", ts.variable_name + f" [{ts.units}]"])
        for i in range(len(ts.values)):
            writer.writerow([ts.timestamps[i].isoformat(), ts.values[i]])

    return ExportTimeseriesResult(
        path=str(csv_path),
        variable_name=ts.variable_name,
        key_value=ts.key_value,
        units=ts.units,
        frequency=ts.frequency,
        rows=len(ts.values),
    )


# Annotations are defined after functions to avoid forward-reference errors.
_TOOL_REGISTRY = [
    (run_simulation, _RUN),
    (get_results_summary, _READ_ONLY),
    (list_output_variables, _READ_ONLY),
    (query_timeseries, _READ_ONLY),
    (export_timeseries, _EXPORT),
]


def register(mcp: FastMCP) -> None:
    """Register simulation tools on the MCP server."""
    for func, hints in _TOOL_REGISTRY:
        mcp.tool(annotations=hints, structured_output=True)(func)
