from __future__ import annotations

import json

from app.core.agent.processes.v2.workflows.doc_update_from_feature.steps.docs_state_loader import DocsState
from app.core.agent.processes.v2.workflows.doc_update_from_feature.steps.plan_hints import (
    PlanUnitHint,
    parse_plan_hints,
)
from app.core.agent.processes.v2.workflows.doc_update_from_feature.steps.plan_path_policy import PlanPathPolicy
from app.core.agent.processes.v2.workflows.doc_update_from_feature.workflow_runtime.context import DocUpdateFromFeatureContext
from app.core.agent.processes.v2.workflows.doc_update_from_feature.workflow_runtime.models import PlannedChange
from app.core.agent.processes.v2.workflows.doc_update_from_feature.workflow_runtime.system_rules import (
    ALLOWED_DOC_TYPES,
    DOC_TYPE_TO_FOLDER,
    SYSTEM_RULES_TEXT,
)
from app.core.agent.utils.llm import AgentLlmService
from app.core.agent.utils.workflow import WorkflowStep
from app.core.rag.persistence.query_repository import RagQueryRepository


class BuildChangePlanStep(WorkflowStep[DocUpdateFromFeatureContext]):
    step_id = "build_change_plan"
    title = "Построение плана изменений"

    def __init__(self, llm: AgentLlmService, query_repository: RagQueryRepository | None = None) -> None:
        self._llm = llm
        self._query_repository = query_repository or RagQueryRepository()
        self._path_policy = PlanPathPolicy(DOC_TYPE_TO_FOLDER)

    async def run(self, context: DocUpdateFromFeatureContext) -> DocUpdateFromFeatureContext:
        if context.answer or not context.units:
            return context
        self._load_docs_state(context)
        inferred_hints = self._infer_plan_hints(context)
        state = DocsState.from_rows(context.docs_catalog_rows)
        for index, unit in enumerate(context.units):
            planned = self._build_unit_plan(context, unit, state, inferred_hints.get(index, PlanUnitHint()))
            if planned is None:
                continue
            context.planned_changes.append(planned)
        if not context.planned_changes:
            context.issues.append("Не удалось построить план изменений по функциональным требованиям.")
        return context

    def trace_output(self, context: DocUpdateFromFeatureContext) -> dict[str, object]:
        return {
            "docs_rows": len(context.docs_catalog_rows),
            "planned_changes": len(context.planned_changes),
            "issues": len(context.issues),
        }

    def _load_docs_state(self, context: DocUpdateFromFeatureContext) -> None:
        if not context.rag_session_id:
            context.issues.append("Отсутствует active RAG session: невозможно сопоставить изменения с текущей документацией.")
            return
        try:
            context.docs_catalog_rows = self._query_repository.list_docs_scope_index_rows(context.rag_session_id)
        except Exception as exc:
            context.issues.append(f"Не удалось загрузить состояние документации из RAG: {exc}")

    def _infer_plan_hints(self, context: DocUpdateFromFeatureContext) -> dict[int, PlanUnitHint]:
        items = []
        for idx, unit in enumerate(context.units):
            items.append(
                {
                    "index": idx,
                    "heading": unit.heading,
                    "snippet": unit.body[:400],
                    "known": {
                        "type": str(unit.metadata.get("type") or "").strip(),
                        "id": str(unit.metadata.get("id") or "").strip(),
                        "application": str(unit.metadata.get("application") or context.analytics_meta.application or "").strip(),
                        "platform": str(unit.metadata.get("platform") or context.analytics_meta.platform or "").strip(),
                    },
                }
            )
        payload = {
            "system_rules": SYSTEM_RULES_TEXT,
            "allowed_doc_types": list(ALLOWED_DOC_TYPES),
            "items": items,
        }
        raw = self._llm.generate(
            "v2_docs_update.plan_change_units",
            json.dumps(payload, ensure_ascii=False, indent=2),
            log_context="workflow.v2.docs_update.from_feature.plan",
            trace=context.runtime.trace.module("workflow.v2.docs_update.from_feature.llm"),
        )
        return parse_plan_hints(raw, ALLOWED_DOC_TYPES)

    def _build_unit_plan(
        self,
        context: DocUpdateFromFeatureContext,
        unit,
        state: DocsState,
        hint: PlanUnitHint,
    ) -> PlannedChange | None:
        doc_type = str(unit.metadata.get("type") or hint.doc_type).strip()
        if doc_type not in ALLOWED_DOC_TYPES:
            context.issues.append(f"Unit '{unit.heading}': неизвестный или отсутствующий type '{doc_type}'.")
            return None
        unit_id = self._path_policy.make_doc_id(
            doc_type=doc_type,
            heading=unit.heading,
            hinted_doc_id=str(unit.metadata.get("id") or hint.doc_id or "").strip(),
        )
        op_hint = str(unit.metadata.get("op") or "create_or_update").strip().lower()
        application = str(unit.metadata.get("application") or context.analytics_meta.application or hint.application).strip()
        platform = str(unit.metadata.get("platform") or context.analytics_meta.platform or hint.platform).strip().lower()
        page_type = str(unit.metadata.get("page_type") or hint.page_type or self._path_policy.default_page_type(doc_type)).strip()
        path = self._path_policy.resolve_path(
            doc_type=doc_type,
            unit_id=unit_id,
            application=application,
            platform=platform,
            page_type=page_type,
            inferred_path=hint.path,
            state=state,
        )
        op = self._path_policy.resolve_op(op_hint=op_hint, unit_id=unit_id, path=path, state=state)
        source_refs = self._as_list(unit.metadata.get("source_refs")) or ["section: 5. Функциональные требования"]
        related_docs = self._as_list(unit.metadata.get("related_docs"))
        reason = f"Из unit '{unit.heading}' системной аналитики ({context.analytics_meta.analysis_id or 'analysis'})."
        return PlannedChange(
            op=op,
            path=path,
            doc_type=doc_type,
            reason=reason,
            title=unit.heading,
            doc_id=unit_id,
            requirement_body=str(unit.body or "").strip(),
            source_refs=source_refs,
            related_docs=related_docs,
        )

    def _as_list(self, value: object) -> list[str]:
        if isinstance(value, list):
            return [str(item).strip() for item in value if str(item).strip()]
        if isinstance(value, str) and value.strip():
            return [value.strip()]
        return []
