"""Agent 可调用的专业软件任务工具。"""
from __future__ import annotations

import json
from uuid import UUID, uuid4

from core.software_contracts import (
    get_contract,
    get_contracts,
    node_available_slots,
    supported_capabilities,
)
from core.software_jobs import (
    SoftwareJobError,
    create_job,
    get_job,
    get_job_request,
    list_jobs,
    request_job_cancel,
    request_job_export,
    revise_job,
)
from core.software_nodes import list_nodes

from .base import Tool


def _contract_property_schema(name: str) -> dict:
    schemas = [
        item.submission_schema()["properties"][name]
        for item in get_contracts().values()
    ]
    unique = {json.dumps(item, ensure_ascii=False, sort_keys=True): item for item in schemas}
    values = list(unique.values())
    return values[0] if len(values) == 1 else {"anyOf": values}


class _SoftwareJobTool(Tool):
    def __init__(self, user_id: UUID, task_id: UUID, **kwargs) -> None:
        super().__init__(**kwargs)
        self.user_id = user_id
        self.task_id = task_id


class SoftwareCapabilityListTool(_SoftwareJobTool):
    name = "software_capability_list"
    description = (
        "List professional software capabilities available through managed Windows nodes. "
        "Use this tool to answer whether software is available; checking availability must "
        "not submit a software job."
    )
    parameters = {"type": "object", "properties": {}, "additionalProperties": False}

    def execute(self) -> str:
        nodes = list_nodes()
        items = [{
            "capability": item,
            "display_name": get_contract(item).display_name,
            "available_nodes": sum(
                1
                for node in nodes
                if node["status"] == "online"
                and item in (node.get("capabilities") or [])
                and node_available_slots(item, node.get("runtime") or {}) > 0
            ),
        } for item in supported_capabilities()]
        items.sort(key=lambda item: (-item["available_nodes"], item["capability"]))
        return json.dumps({"capabilities": items}, ensure_ascii=False)


class SoftwareJobSubmitTool(_SoftwareJobTool):
    name = "software_job_submit"
    description = (
        "Submit a managed professional-software job using registered artifacts and a "
        "capability contract only when the user requested actual software execution; never "
        "submit a job merely to check whether software is available. Call register_artifact "
        "first for workspace files. A successful "
        "submission is the terminal action for the current run: report the queued job_id once "
        "and end the turn. Completion delivery and artifact publication happen automatically "
        "in a later system-managed message."
    )
    @staticmethod
    def _parameters() -> dict:
        return {
            "type": "object",
            "properties": {
                "capability": {
                    "type": "string", "enum": sorted(supported_capabilities())
                },
                "inputs": _contract_property_schema("inputs"),
                "operation": _contract_property_schema("operation"),
                "outputs": _contract_property_schema("outputs"),
                "completion_action": {
                    "type": "string",
                    "enum": ["report", "analyze"],
                    "default": "report",
                    "description": (
                        "Use report to notify with output files only. Use analyze when the "
                        "user also asked for interpretation, conclusions, or data analysis."
                    ),
                },
                "idempotency_key": {
                    "type": "string",
                    "description": (
                        "Stable unique key for this exact submission; omit to generate one."
                    ),
                },
            },
            "required": ["capability", "inputs", "operation", "outputs"],
            "additionalProperties": False,
        }

    # Compatibility for callers inspecting the class; Tool.schema below is always fresh.
    parameters = _parameters()

    @property
    def schema(self) -> dict:
        value = super().schema
        value["function"]["parameters"] = self._parameters()
        return value

    def execute(
        self,
        capability: str,
        inputs: list[dict] | None = None,
        operation: dict | None = None,
        outputs: list[dict] | None = None,
        completion_action: str = "report",
        idempotency_key: str = "",
    ) -> str:
        try:
            if inputs is None or operation is None or outputs is None:
                return "[Error] inputs, operation, and outputs are required for a software job"
            canonical_inputs = []
            for item in inputs:
                if not isinstance(item, dict):
                    return "[Error] each input must be an object"
                try:
                    artifact_id = str(UUID(str(item.get("artifact_id") or "")))
                except ValueError:
                    return "[Error] every artifact_id must be a UUID; call register_artifact first"
                canonical = {**item, "artifact_id": artifact_id}
                canonical_inputs.append(canonical)
            normalized_request = {
                "schema_version": get_contract(capability).request_schema[
                    "properties"
                ]["schema_version"]["const"],
                "inputs": canonical_inputs,
                "operation": operation,
                "outputs": outputs,
            }
            job, created = create_job(
                self.user_id,
                self.task_id,
                idempotency_key=idempotency_key.strip() or str(uuid4()),
                capability=capability,
                completion_action=completion_action,
                request=normalized_request,
            )
            return json.dumps({
                **job,
                "created": created,
                "completion_delivery": "automatic",
                "next_action": "end_turn_after_reporting_queued_job_id",
            }, ensure_ascii=False)
        except (SoftwareJobError, ValueError) as exc:
            return f"[Error] {exc}"


class SoftwareJobStatusTool(_SoftwareJobTool):
    name = "software_job_status"
    description = (
        "Check one software job, or list recent jobs in the current task when job_id is omitted. "
        "A succeeded workspace job returns preview_manifest and local_manifest. Local outputs "
        "remain on the Windows node; call software_job_export only after an explicit user "
        "request to download or deliver selected files."
    )
    parameters = {
        "type": "object",
        "properties": {"job_id": {"type": "string"}},
        "additionalProperties": False,
    }

    def execute(self, job_id: str = "") -> str:
        try:
            if job_id.strip():
                item = get_job(self.user_id, UUID(job_id.strip()))
                if item is None or item["task_id"] != str(self.task_id):
                    return "[Error] software job not found"
                item["editable_request"] = get_job_request(
                    self.user_id, self.task_id, UUID(job_id.strip())
                )
                return json.dumps(item, ensure_ascii=False)
            return json.dumps(
                {"results": list_jobs(self.user_id, task_id=self.task_id, limit=20)},
                ensure_ascii=False,
            )
        except ValueError:
            return "[Error] invalid job_id"


class SoftwareJobReviseTool(_SoftwareJobTool):
    name = "software_job_revise"
    description = (
        "Create a new professional-software job from a prior job in the current task. "
        "Continue from the prior job's node-local workspace state, reuse its registered inputs, "
        "and provide a complete next operation and outputs. Call software_job_status "
        "first when the prior editable_request is not already in context. A successful revision "
        "is the terminal action for the current run; completion and artifacts are delivered "
        "automatically in a later system-managed message."
    )

    @staticmethod
    def _parameters() -> dict:
        return {
            "type": "object",
            "properties": {
                "source_job_id": {"type": "string", "format": "uuid"},
                "operation": _contract_property_schema("operation"),
                "outputs": _contract_property_schema("outputs"),
                "idempotency_key": {
                    "type": "string",
                    "description": (
                        "Stable unique key for this exact revision; omit to generate one."
                    ),
                },
            },
            "required": ["source_job_id", "operation", "outputs"],
            "additionalProperties": False,
        }

    parameters = _parameters()

    @property
    def schema(self) -> dict:
        value = super().schema
        value["function"]["parameters"] = self._parameters()
        return value

    def execute(
        self,
        source_job_id: str,
        operation: dict | None = None,
        outputs: list[dict] | None = None,
        idempotency_key: str = "",
    ) -> str:
        try:
            if operation is None or not outputs:
                return "[Error] operation and outputs are required for a software revision"
            job, created = revise_job(
                self.user_id,
                self.task_id,
                UUID(source_job_id.strip()),
                idempotency_key=idempotency_key.strip() or str(uuid4()),
                operation=operation,
                outputs=outputs,
            )
            return json.dumps({
                **job,
                "created": created,
                "completion_delivery": "automatic",
                "next_action": "end_turn_after_reporting_queued_job_id",
            }, ensure_ascii=False)
        except (SoftwareJobError, ValueError) as exc:
            return f"[Error] {exc}"


class SoftwareJobCancelTool(_SoftwareJobTool):
    name = "software_job_cancel"
    description = "Request cancellation of a software job in the current task after the user asks to stop it."
    parameters = {
        "type": "object",
        "properties": {"job_id": {"type": "string"}},
        "required": ["job_id"],
        "additionalProperties": False,
    }

    def execute(self, job_id: str) -> str:
        try:
            item = get_job(self.user_id, UUID(job_id.strip()))
            if item is None or item["task_id"] != str(self.task_id):
                return "[Error] software job not found"
            job, _ = request_job_cancel(self.user_id, UUID(job_id.strip()))
            return json.dumps(job, ensure_ascii=False)
        except (ValueError, SoftwareJobError) as exc:
            return f"[Error] {exc}"


class SoftwareJobExportTool(_SoftwareJobTool):
    name = "software_job_export"
    description = (
        "Export selected node-local outputs from the current successful workspace head. "
        "Call this only when the user explicitly asks to download, receive, or deliver files. "
        "The transfer is asynchronous and completion is reported automatically."
    )
    parameters = {
        "type": "object",
        "properties": {
            "job_id": {"type": "string", "format": "uuid"},
            "output_ids": {
                "type": "array",
                "minItems": 1,
                "maxItems": 16,
                "uniqueItems": True,
                "items": {"type": "string"},
                "description": "Output identities from software_job_status.local_manifest.",
            },
        },
        "required": ["job_id", "output_ids"],
        "additionalProperties": False,
    }

    def execute(self, job_id: str, output_ids: list[str] | None = None) -> str:
        try:
            current = get_job(self.user_id, UUID(job_id.strip()))
            if current is None or current["task_id"] != str(self.task_id):
                return "[Error] software job not found"
            job = request_job_export(
                self.user_id,
                UUID(job_id.strip()),
                output_ids or [],
            )
            return json.dumps({
                **job,
                "export_delivery": "automatic",
                "next_action": "end_turn_after_reporting_export_queued",
            }, ensure_ascii=False)
        except (ValueError, SoftwareJobError) as exc:
            return f"[Error] {exc}"
