# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2026 Detent Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Session manager for coordinating checkpoint, pipeline, and IPC."""

from __future__ import annotations

import asyncio
from datetime import UTC, datetime
from typing import TYPE_CHECKING

import structlog

if TYPE_CHECKING:
    from detent.checkpoint.engine import CheckpointEngine
    from detent.ipc.channel import IPCControlChannel
    from detent.pipeline.pipeline import VerificationPipeline
    from detent.schema import AgentAction

from detent.feedback.synthesizer import FeedbackSynthesizer
from detent.ipc.schemas import IPCMessage, IPCMessageType
from detent.observability.metrics import record_rollback, record_tool_call
from detent.observability.tracer import get_tracer
from detent.pipeline.result import VerificationResult
from detent.proxy.types import DetentSessionConflictError, SessionState

logger: structlog.stdlib.BoundLogger = structlog.get_logger()


class SessionManager:
    """Manages verification session lifecycle.

    Coordinates:
    - Session state (start, active, end)
    - SAVEPOINT creation before tool calls
    - Pipeline execution
    - Feedback synthesis
    - IPC message dispatch
    """

    def __init__(
        self,
        checkpoint_engine: CheckpointEngine,
        pipeline: VerificationPipeline,
        ipc_channel: IPCControlChannel,
        synthesizer: FeedbackSynthesizer | None = None,
    ) -> None:
        """Initialize session manager.

        Args:
            checkpoint_engine: Checkpoint engine for SAVEPOINTs
            pipeline: Verification pipeline
            ipc_channel: IPC control channel
            synthesizer: Feedback synthesizer (optional)
        """
        self.checkpoint_engine = checkpoint_engine
        self.pipeline = pipeline
        self.ipc_channel = ipc_channel
        self.synthesizer = synthesizer or FeedbackSynthesizer()

        self._state: SessionState | None = None
        self._lock = asyncio.Lock()

    @property
    def is_active(self) -> bool:
        """Whether a session is currently active."""
        return self._state is not None

    @property
    def session_id(self) -> str | None:
        """Active session ID, or None."""
        return self._state.session_id if self._state else None

    async def start_session(self, session_id: str) -> None:
        """Start a verification session.

        Args:
            session_id: Unique session identifier

        Raises:
            DetentSessionConflictError if session already active
        """
        async with self._lock:
            if self._state is not None:
                raise DetentSessionConflictError(f"Session already active: {self._state.session_id}")

            self._state = SessionState(
                session_id=session_id,
                started_at=datetime.now(UTC).isoformat(),
            )

        logger.info("[session] started session %s", session_id)

        # Notify IPC
        await self.ipc_channel.send_message(
            IPCMessage(
                type=IPCMessageType.SESSION_START,
                data={"session_id": session_id},
                timestamp=datetime.now(UTC).isoformat(),
            )
        )

    async def end_session(self) -> None:
        """End the current verification session."""
        if not self.is_active:
            return

        session_id = self.session_id

        async with self._lock:
            self._state = None

        logger.info("[session] ended session %s", session_id)

        # Notify IPC
        await self.ipc_channel.send_message(
            IPCMessage(
                type=IPCMessageType.SESSION_END,
                data={"session_id": session_id},
                timestamp=datetime.now(UTC).isoformat(),
            )
        )

    async def intercept_tool_call(self, action: AgentAction | None) -> VerificationResult:
        """Intercept and verify a tool call.

        Args:
            action: Normalized agent action

        Returns:
            Verification result (pass/fail with findings)
        """
        if action is None:
            logger.debug("[session] no action to verify; skipping pipeline")
            return VerificationResult(
                stage="session",
                passed=True,
                findings=[],
                duration_ms=0.0,
            )
        if self._state is None:
            logger.warning("[session] tool call intercepted but no active session")
            return VerificationResult(
                stage="session",
                passed=False,
                findings=[],
                duration_ms=0.0,
            )

        # Create checkpoint reference
        checkpoint_ref = f"chk_before_write_{len(self._state.checkpoint_refs):03d}"
        tracer = get_tracer(__name__)
        span_attrs = {
            "detent.session_id": self.session_id,
            "detent.tool_call_id": action.tool_call_id,
            "detent.action_type": action.action_type,
            "detent.agent": action.agent,
            "detent.risk_level": action.risk_level.value,
            "detent.file_path": action.file_path or "<unknown>",
        }

        try:
            with tracer.start_as_current_span("detent.tool_call", attributes=span_attrs) as span:
                # Create SAVEPOINT before running pipeline
                files = [action.file_path] if action.file_path else []
                await self.checkpoint_engine.savepoint(checkpoint_ref, files)

                async with self._lock:
                    self._state.checkpoint_refs.append(checkpoint_ref)
                    self._state.active_tool_call_id = action.tool_call_id

                logger.info("[session] created checkpoint %s", checkpoint_ref)

                # Update action with checkpoint ref
                action.checkpoint_ref = checkpoint_ref
                span.set_attribute("detent.checkpoint_ref", checkpoint_ref)

                # Notify IPC of intercepted tool
                await self.ipc_channel.send_message(
                    IPCMessage(
                        type=IPCMessageType.TOOL_INTERCEPTED,
                        data={
                            "tool_call_id": action.tool_call_id,
                            "action": action.model_dump(),
                        },
                        timestamp=datetime.now(UTC).isoformat(),
                    )
                )

                # Run verification pipeline
                result = await self.pipeline.run(action)

                span.set_attribute("detent.tool_call.passed", result.passed)
                record_tool_call(action.agent, action.action_type.value, result.passed)

                if result.passed:
                    await self._on_verification_pass(action, result)
                else:
                    await self._on_verification_fail(action, result, checkpoint_ref)

                return result
        except Exception as e:
            logger.error("[session] tool interception failed: %s", e)
            return VerificationResult(
                stage="session",
                passed=False,
                findings=[],
                duration_ms=0.0,
            )

    async def observe_tool_call(self, action: AgentAction | None) -> VerificationResult:
        """Observe a tool intent without enforcing checkpoint or rollback behavior.

        Point 1 (HTTP proxy) only sees model intent from the LLM response body.
        It cannot block or modify execution, so this path runs the pipeline
        speculatively and emits IPC notifications without creating savepoints.
        """
        if action is None:
            logger.debug("[session] no action to observe; skipping pipeline")
            return VerificationResult(
                stage="session",
                passed=True,
                findings=[],
                duration_ms=0.0,
            )
        if self._state is None:
            logger.warning("[session] tool intent observed but no active session")
            return VerificationResult(
                stage="session",
                passed=False,
                findings=[],
                duration_ms=0.0,
            )

        if not action.session_id:
            action.session_id = self.session_id or ""

        tracer = get_tracer(__name__)
        span_attrs = {
            "detent.session_id": self.session_id,
            "detent.tool_call_id": action.tool_call_id,
            "detent.action_type": action.action_type,
            "detent.agent": action.agent,
            "detent.risk_level": action.risk_level.value,
            "detent.file_path": action.file_path or "<unknown>",
            "detent.observational": True,
        }

        try:
            with tracer.start_as_current_span("detent.tool_intent", attributes=span_attrs) as span:
                await self.ipc_channel.send_message(
                    IPCMessage(
                        type=IPCMessageType.TOOL_INTERCEPTED,
                        data={
                            "tool_call_id": action.tool_call_id,
                            "action": action.model_dump(),
                            "mode": "observation",
                        },
                        timestamp=datetime.now(UTC).isoformat(),
                    )
                )

                result = await self.pipeline.run(action)

                span.set_attribute("detent.tool_call.passed", result.passed)
                record_tool_call(action.agent, action.action_type.value, result.passed)

                logger.info(
                    "[session] observational verification %s for tool %s",
                    "passed" if result.passed else "failed",
                    action.tool_name,
                )

                await self.ipc_channel.send_message(
                    IPCMessage(
                        type=IPCMessageType.VERIFICATION_RESULT,
                        data={
                            "tool_call_id": action.tool_call_id,
                            "status": "observed",
                            "passed": result.passed,
                            "checkpoint_ref": "",
                            "mode": "observation",
                        },
                        timestamp=datetime.now(UTC).isoformat(),
                    )
                )

                return result
        except Exception as e:
            logger.error("[session] observational tool verification failed: %s", e)
            return VerificationResult(
                stage="session",
                passed=False,
                findings=[],
                duration_ms=0.0,
            )

    async def _on_verification_pass(
        self,
        action: AgentAction,
        result: VerificationResult,
    ) -> None:
        """Handle verification pass."""
        logger.info(
            "[session] verification passed for tool %s",
            action.tool_name,
        )

        # Notify IPC
        await self.ipc_channel.send_message(
            IPCMessage(
                type=IPCMessageType.VERIFICATION_RESULT,
                data={
                    "tool_call_id": action.tool_call_id,
                    "status": "allowed",
                    "checkpoint_ref": action.checkpoint_ref,
                },
                timestamp=datetime.now(UTC).isoformat(),
            )
        )

    async def _on_verification_fail(
        self,
        action: AgentAction,
        result: VerificationResult,
        checkpoint_ref: str,
    ) -> None:
        """Handle verification failure."""
        record_rollback(result.stage)
        logger.info(
            "[session] verification failed for tool %s, rolling back",
            action.tool_name,
        )

        # Synthesize feedback
        feedback = self.synthesizer.synthesize(result, action)

        # Rollback checkpoint
        await self.checkpoint_engine.rollback(checkpoint_ref)

        # Notify IPC of rollback
        await self.ipc_channel.send_message(
            IPCMessage(
                type=IPCMessageType.ROLLBACK_INSTRUCTION,
                data={
                    "tool_call_id": action.tool_call_id,
                    "checkpoint_ref": checkpoint_ref,
                    "feedback": feedback.model_dump(),
                },
                timestamp=datetime.now(UTC).isoformat(),
            )
        )
