"""Ploston Application - orchestrator for all components.

This is the main application class that initializes and wires all
components together. It is the equivalent of AELApplication from
the original AEL codebase.

Both ploston (OSS) and ploston-enterprise use this class for
proper component initialization.
"""

import logging
import os
import sys
from typing import TYPE_CHECKING, Any, TextIO

from ploston_core.config import ConfigLoader, MCPHTTPConfig
from ploston_core.engine import WorkflowEngine
from ploston_core.errors import ErrorFactory, ErrorRegistry
from ploston_core.invoker import SandboxFactory, ToolInvoker
from ploston_core.logging import AELLogger, LogConfig
from ploston_core.mcp import MCPClientManager
from ploston_core.mcp_frontend import MCPFrontend, MCPServerConfig
from ploston_core.registry import ToolRegistry
from ploston_core.sandbox import SandboxConfig
from ploston_core.telemetry import (
    ChainDetector,
    ChainDetectorConfig,
    OTLPExporterConfig,
    TelemetryConfig,
    get_telemetry,
    setup_telemetry,
)
from ploston_core.telemetry.store import (
    TelemetryStore,
    TelemetryStoreConfig,
    create_telemetry_store,
)
from ploston_core.telemetry.store.collector import TelemetryCollector
from ploston_core.template import TemplateEngine
from ploston_core.types import MCPTransport
from ploston_core.workflow import WorkflowRegistry

if TYPE_CHECKING:
    from ploston_core.config.redis_store import RedisConfigStore
    from ploston_core.runner_management import RunnerRegistry


class PlostApplication:
    """
    Ploston Application orchestrator.

    Initializes and wires all components together. This class handles
    the full initialization sequence including:

    1. Config loading
    2. Logger setup
    3. Telemetry setup
    4. Error registry
    5. MCP client manager (connects to MCP servers)
    6. Tool registry (discovers tools from MCP servers)
    7. Workflow registry (loads workflows from config)
    8. Sandbox factory (Python execution)
    9. Template engine (Jinja2)
    10. Tool invoker
    11. Workflow engine
    12. MCP frontend (server)
    """

    def __init__(
        self,
        config_path: str | None = None,
        log_output: TextIO | None = None,
        transport: MCPTransport = MCPTransport.HTTP,
        http_host: str = "0.0.0.0",
        http_port: int = 8022,
        with_rest_api: bool = True,
        rest_api_prefix: str = "/api/v1",
        rest_api_docs: bool = True,
    ):
        """Initialize application.

        Args:
            config_path: Path to config file (optional)
            log_output: Output stream for logs (default: sys.stdout)
            transport: MCP transport type (default: HTTP for server mode)
            http_host: HTTP host (default: 0.0.0.0)
            http_port: HTTP port (default: 8022)
            with_rest_api: Enable REST API alongside MCP server (default: True)
            rest_api_prefix: URL prefix for REST API (default: /api/v1)
            rest_api_docs: Enable OpenAPI docs at /docs (default: True)
        """
        self._config_path = config_path
        self._log_output = log_output or sys.stdout
        self._transport = transport
        self._http_host = http_host
        self._http_port = http_port
        self._with_rest_api = with_rest_api
        self._rest_api_prefix = rest_api_prefix
        self._rest_api_docs = rest_api_docs
        self._initialized = False

        # Components (initialized in initialize())
        self.config_loader: ConfigLoader | None = None
        self.config: Any = None
        self.logger: AELLogger | None = None
        self.error_registry: ErrorRegistry | None = None
        self.error_factory: ErrorFactory | None = None
        self.mcp_manager: MCPClientManager | None = None
        self.tool_registry: ToolRegistry | None = None
        self.workflow_registry: WorkflowRegistry | None = None
        self.sandbox_factory: SandboxFactory | None = None
        self.template_engine: TemplateEngine | None = None
        self.tool_invoker: ToolInvoker | None = None
        self.workflow_engine: WorkflowEngine | None = None
        self.mcp_frontend: MCPFrontend | None = None
        self.redis_config_store: RedisConfigStore | None = None
        self.runner_registry: RunnerRegistry | None = None
        self.telemetry_store: TelemetryStore | None = None
        self.chain_detector: ChainDetector | None = None
        self.telemetry_collector: TelemetryCollector | None = None

    async def initialize(self) -> None:
        """Initialize all components.

        This method performs the full initialization sequence:
        1. Load configuration
        2. Set up logging
        3. Set up telemetry
        4. Create error handling
        5. Connect to MCP servers
        6. Discover tools
        7. Load workflows
        8. Set up execution environment
        9. Create MCP frontend
        """
        if self._initialized:
            return

        # 1. Config Loader
        self.config_loader = ConfigLoader()
        self.config = self.config_loader.load(self._config_path)

        # 1b. Root stdlib logger — ensure all getLogger() loggers in the CP
        # process have a handler that writes to stdout so output appears in
        # `docker logs`.  Respects PLOSTON_LOG_LEVEL env var (set in
        # docker-compose) and falls back to the config file's logging level.
        _root = logging.getLogger()
        if not _root.handlers:
            _console = logging.StreamHandler(sys.stdout)
            _console.setFormatter(
                logging.Formatter("%(asctime)s %(levelname)-8s [%(name)s] %(message)s")
            )
            _root.addHandler(_console)
        # Map config level string to stdlib int; default to INFO
        _level_name = os.environ.get(
            "PLOSTON_LOG_LEVEL",
            self.config.logging.level.value
            if hasattr(self.config.logging.level, "value")
            else str(self.config.logging.level),
        ).upper()
        _root.setLevel(getattr(logging, _level_name, logging.INFO))

        # 2. Logger
        log_config = LogConfig(
            level=self.config.logging.level,
            format=self.config.logging.format,
            show_params=self.config.logging.options.show_params,
            show_results=self.config.logging.options.show_results,
            truncate_at=self.config.logging.options.truncate_at,
            components={
                "workflow": self.config.logging.components.workflow,
                "step": self.config.logging.components.step,
                "tool": self.config.logging.components.tool,
                "sandbox": self.config.logging.components.sandbox,
            },
            output=self._log_output,
        )
        self.logger = AELLogger(log_config)

        # 3. Telemetry Setup
        telemetry_enabled = (
            os.environ.get("PLOSTON_TELEMETRY_ENABLED", "").lower() == "true"
            or self.config.telemetry.enabled
        )
        metrics_enabled = (
            os.environ.get("PLOSTON_METRICS_ENABLED", "").lower() == "true"
            or self.config.telemetry.metrics.enabled
        )
        traces_enabled = (
            os.environ.get("PLOSTON_TRACES_ENABLED", "").lower() == "true"
            or self.config.telemetry.tracing.enabled
        )
        logs_enabled = (
            os.environ.get("PLOSTON_LOGS_ENABLED", "").lower() == "true"
            or self.config.telemetry.logging.enabled
        )

        otlp_endpoint = os.environ.get(
            "OTEL_EXPORTER_OTLP_ENDPOINT",
            self.config.telemetry.export.otlp.endpoint,
        )
        otlp_enabled = bool(otlp_endpoint) and (traces_enabled or logs_enabled)

        telemetry_config = TelemetryConfig(
            enabled=telemetry_enabled,
            service_name=os.environ.get("OTEL_SERVICE_NAME", self.config.telemetry.service_name),
            service_version=self.config.telemetry.service_version,
            metrics_enabled=metrics_enabled,
            traces_enabled=traces_enabled,
            traces_sample_rate=self.config.telemetry.tracing.sample_rate,
            logs_enabled=logs_enabled,
            otlp=OTLPExporterConfig(
                enabled=otlp_enabled,
                endpoint=otlp_endpoint,
                insecure=os.environ.get("OTEL_EXPORTER_OTLP_INSECURE", "true").lower() == "true",
                protocol=self.config.telemetry.export.otlp.protocol,
                headers=self.config.telemetry.export.otlp.headers,
            ),
        )
        telemetry = setup_telemetry(telemetry_config)

        # 3a-bis. Python logging → OTEL bridge (DEC-149)
        # When logs are enabled, LoggingInstrumentor.instrument() attaches an
        # OTEL LoggingHandler to the root Python logger so that stdlib logging
        # output is forwarded to the OTEL LoggerProvider and ultimately to
        # Loki via the collector.  It also injects trace-context fields into
        # log records.
        #
        # We must also lower the root logger level to DEBUG so that INFO-level
        # records from AELLogger's stdlib bridge are not filtered out before
        # they reach the handler.
        if telemetry_config.logs_enabled and telemetry.get("logger_provider"):
            try:
                from opentelemetry.instrumentation.logging import LoggingInstrumentor

                LoggingInstrumentor().instrument(set_logging_format=False)
                # Ensure root logger level doesn't filter out INFO/DEBUG
                # records before they reach the OTEL handler.
                root_logger = logging.getLogger()
                if root_logger.level > logging.DEBUG:
                    root_logger.setLevel(logging.DEBUG)
            except ImportError:
                logging.getLogger(__name__).debug(
                    "opentelemetry-instrumentation-logging not installed; skipping OTEL log bridge"
                )

        # 3b. Telemetry Store (DEC-148: canonical execution store)
        telemetry_store_path = os.environ.get(
            "TELEMETRY_STORE_SQLITE_PATH",
            "./data/telemetry.db",
        )
        store_config = TelemetryStoreConfig(
            enabled=True,
            storage_type="sqlite",
            sqlite_path=telemetry_store_path,
        )
        self.telemetry_store = create_telemetry_store(store_config)

        # 3c. Telemetry Collector (DEC-152: wraps store for execution lifecycle)
        self.telemetry_collector = TelemetryCollector(
            store=self.telemetry_store,
            config=store_config,
        )

        # 4. Error Registry & Factory
        self.error_registry = ErrorRegistry()
        self.error_factory = ErrorFactory(self.error_registry)

        # 5. MCP Client Manager
        self.mcp_manager = MCPClientManager(
            self.config.tools,
            logger=self.logger,
        )

        # 5b. Redis Config Store & Runner Registry (if Redis URL is configured)
        redis_url = os.environ.get("PLOSTON_REDIS_URL") or os.environ.get("AEL_REDIS_URL")
        if redis_url:
            from ploston_core.config.redis_store import (
                RedisConfigStore,
                RedisConfigStoreOptions,
            )
            from ploston_core.runner_management import PersistentRunnerRegistry

            redis_options = RedisConfigStoreOptions(redis_url=redis_url)
            self.redis_config_store = RedisConfigStore(redis_options)
            await self.redis_config_store.connect()
            self.runner_registry = PersistentRunnerRegistry(
                self.redis_config_store,
                config_file_path=self._config_path,
            )
            await self.runner_registry.load_from_redis()

            # Sync runners from config file if any are defined
            if self.config.runners:
                # Convert RunnerDefinition dataclasses to dicts for sync_from_config
                runners_dict = {}
                for name, runner_def in self.config.runners.items():
                    runners_dict[name] = {
                        "mcp_servers": {
                            mcp_name: {
                                k: v
                                for k, v in mcp_def.__dict__.items()
                                if v is not None and v != {} and v != []
                            }
                            for mcp_name, mcp_def in runner_def.mcp_servers.items()
                        }
                        if runner_def.mcp_servers
                        else {}
                    }
                sync_results = await self.runner_registry.sync_from_config(runners_dict)
                if self.logger:
                    from ploston_core.types.enums import LogLevel

                    created_count = sum(1 for r in sync_results.values() if r.get("created"))
                    self.logger._log(
                        LogLevel.INFO,
                        "runner",
                        f"Synced {len(runners_dict)} runners from config ({created_count} created)",
                        {"runners": list(runners_dict.keys())},
                    )

            if self.logger:
                from ploston_core.types.enums import LogLevel

                self.logger._log(
                    LogLevel.INFO,
                    "runner",
                    "Runner registry initialized with Redis persistence",
                    {"redis_url": redis_url.split("@")[-1]},  # Hide credentials
                )

        # 6. Tool Registry
        self.tool_registry = ToolRegistry(
            self.mcp_manager,
            self.config.tools,
            logger=self.logger,
        )

        # Wire up telemetry metrics to registries
        telemetry = get_telemetry()
        if telemetry and telemetry.get("metrics"):
            ael_metrics = telemetry["metrics"]
            self.tool_registry.set_metrics(ael_metrics)
            if self.runner_registry:
                self.runner_registry.set_metrics(ael_metrics)

        await self.tool_registry.initialize()

        # 7. Workflow Registry
        self.workflow_registry = WorkflowRegistry(
            self.tool_registry,
            self.config.workflows,
            logger=self.logger,
            redis_store=self.redis_config_store,
            runner_registry=self.runner_registry,
        )
        await self.workflow_registry.initialize()

        # 8. Sandbox Factory
        sandbox_config = SandboxConfig(
            timeout=self.config.python_exec.timeout,
            max_tool_calls=self.config.python_exec.max_tool_calls,
            allowed_imports=self.config.python_exec.default_imports,
        )
        self.sandbox_factory = SandboxFactory(
            sandbox_config,
            logger=self.logger,
        )

        # 9. Template Engine
        self.template_engine = TemplateEngine()

        # 10. Tool Invoker
        # DEC-161: Wire RunnerToolDispatcher so ToolInvoker can dispatch
        # runner-prefixed tools via WebSocket to connected runners.
        runner_dispatcher = None
        if self.runner_registry:
            from ploston_core.runner_management.dispatcher import RunnerToolDispatcher

            runner_dispatcher = RunnerToolDispatcher(self.runner_registry)

        self.tool_invoker = ToolInvoker(
            self.tool_registry,
            self.mcp_manager,
            self.sandbox_factory,
            logger=self.logger,
            error_factory=self.error_factory,
            runner_dispatcher=runner_dispatcher,
        )

        # 11. Workflow Engine
        # Create token estimator for savings metrics if telemetry is enabled
        token_estimator = None
        telemetry = get_telemetry()
        if telemetry and telemetry.get("meter"):
            from ploston_core.telemetry import TokenEstimator

            token_estimator = TokenEstimator(meter=telemetry["meter"])

        # 11b. Create ChainDetector for pattern detection on direct tool calls (DEC-057)
        _app_logger = logging.getLogger("ploston.chain_detection")
        chain_detector = None
        telemetry = get_telemetry()
        if telemetry and telemetry.get("meter"):
            redis_client = None
            if self.redis_config_store and self.redis_config_store.connected:
                redis_client = self.redis_config_store._client  # reuse existing connection
            _app_logger.info(
                "Creating ChainDetector: meter=%s, redis=%s, tracer=%s",
                type(telemetry["meter"]).__name__,
                "connected" if redis_client else "none",
                "present" if telemetry.get("tracer") else "none",
            )
            chain_detector = ChainDetector(
                redis_client=redis_client,
                meter=telemetry["meter"],
                tracer=telemetry.get("tracer"),
                config=ChainDetectorConfig(),
            )
        else:
            _app_logger.warning(
                "ChainDetector NOT created: telemetry=%s, meter=%s",
                "present" if telemetry else "None",
                "present" if (telemetry and telemetry.get("meter")) else "MISSING",
            )
        self.chain_detector = chain_detector

        self.workflow_engine = WorkflowEngine(
            self.workflow_registry,
            self.tool_invoker,
            self.template_engine,
            self.config.execution,
            logger=self.logger,
            error_factory=self.error_factory,
            token_estimator=token_estimator,
            runner_registry=self.runner_registry,
            tool_registry=self.tool_registry,
            max_tool_calls=self.config.python_exec.max_tool_calls,
        )

        # 12. MCP Frontend
        http_config = None
        if self._transport == MCPTransport.HTTP:
            http_config = MCPHTTPConfig(
                host=self._http_host,
                port=self._http_port,
            )

        # Create mode manager based on whether config file exists
        # - RUNNING mode: config file was found and loaded
        # - CONFIGURATION mode: no config file, using defaults (initial setup)
        # NOTE: Must be created BEFORE REST app so config endpoints have access
        from ploston_core.config import Mode, ModeManager, StagedConfig
        from ploston_core.config.tools import ConfigToolRegistry

        initial_mode = Mode.CONFIGURATION if self.config_loader.used_defaults else Mode.RUNNING
        mode_manager = ModeManager(
            initial_mode=initial_mode,
            redis_store=self.redis_config_store,
        )

        # Try to restore mode from Redis (overrides file-based detection)
        # This ensures that if config was pushed via API and mode set to RUNNING,
        # it persists across container restarts
        if self.redis_config_store and self.redis_config_store.connected:
            restored = await mode_manager.restore_mode_from_redis()
            if restored and self.logger:
                from ploston_core.types.enums import LogLevel

                self.logger._log(
                    LogLevel.INFO,
                    "mode",
                    f"Restored mode from Redis: {mode_manager.mode.value}",
                    {},
                )

        # Create staged config for config/set and config/done endpoints
        staged_config = StagedConfig(self.config_loader)

        # Create config tool registry
        config_tool_registry = ConfigToolRegistry(
            staged_config=staged_config,
            config_loader=self.config_loader,
            mode_manager=mode_manager,
            mcp_manager=self.mcp_manager,
            redis_store=self.redis_config_store,
            runner_registry=self.runner_registry,
        )

        # Create REST API app if dual-mode is enabled
        rest_app = None
        if self._with_rest_api and self._transport == MCPTransport.HTTP:
            from ploston_core.api.app import create_rest_app
            from ploston_core.api.config import RESTConfig

            rest_config = RESTConfig(
                prefix="",  # Prefix is handled by mount point
                docs_enabled=self._rest_api_docs,
                docs_path="/docs",
                redoc_path="/redoc",
                openapi_path="/openapi.json",
            )
            rest_app = create_rest_app(
                workflow_registry=self.workflow_registry,
                workflow_engine=self.workflow_engine,
                tool_registry=self.tool_registry,
                tool_invoker=self.tool_invoker,
                config=rest_config,
                logger=self.logger,
                runner_registry=self.runner_registry,
                ael_config=self.config,
                mode_manager=mode_manager,
                staged_config=staged_config,
                config_loader=self.config_loader,
                mcp_manager=self.mcp_manager,
                redis_store=self.redis_config_store,
                telemetry_store=self.telemetry_store,
            )

        # Create workflow CRUD tools provider
        from ploston_core.workflow.tools import WorkflowToolsProvider

        workflow_tools = WorkflowToolsProvider(
            self.workflow_registry,
            tool_registry=self.tool_registry,
            runner_registry=self.runner_registry,
            workflow_engine=self.workflow_engine,
        )

        self.mcp_frontend = MCPFrontend(
            self.workflow_engine,
            self.tool_registry,
            self.workflow_registry,
            self.tool_invoker,
            config=MCPServerConfig(),
            logger=self.logger,
            mode_manager=mode_manager,
            config_tool_registry=config_tool_registry,
            transport=self._transport,
            http_config=http_config,
            rest_app=rest_app,
            rest_prefix=self._rest_api_prefix,
            runner_registry=self.runner_registry,  # DEC-123: Enable runner tool routing
            workflow_tools_provider=workflow_tools,
            chain_detector=self.chain_detector,  # Tier 0 (DEC-057)
            telemetry_collector=self.telemetry_collector,  # Tier 3 (DEC-152)
        )

        _app_logger.info(
            "MCPFrontend wired: chain_detector=%s",
            "ACTIVE" if self.chain_detector else "NONE",
        )

        # Wire tools/list_changed notifications for all tool-list mutation sources
        _notify = self.mcp_frontend._send_tools_changed_notification  # noqa: SLF001
        workflow_tools._on_tools_changed = _notify  # noqa: SLF001
        self.tool_registry._on_tools_changed = _notify  # noqa: SLF001
        if self.runner_registry:
            self.runner_registry._on_tools_changed = _notify  # noqa: SLF001

        self._initialized = True

    async def shutdown(self) -> None:
        """Shutdown all components."""
        if not self._initialized:
            return

        # Disconnect MCP clients
        if self.mcp_manager:
            await self.mcp_manager.disconnect_all()

        self._initialized = False

    def start_watching(self) -> None:
        """Start watching for config/workflow changes (placeholder for hot-reload)."""
        pass

    def stop_watching(self) -> None:
        """Stop watching for changes (placeholder for hot-reload)."""
        pass

    async def run_workflow(
        self,
        workflow_id: str,
        inputs: dict[str, Any],
        timeout_seconds: int | None = None,
    ) -> Any:
        """Run workflow by ID.

        Args:
            workflow_id: Workflow ID
            inputs: Workflow inputs
            timeout_seconds: Optional timeout

        Returns:
            ExecutionResult
        """
        if not self.workflow_engine:
            raise RuntimeError("Application not initialized")

        return await self.workflow_engine.execute(workflow_id, inputs, timeout_seconds)

    async def start(self) -> None:
        """Start the MCP frontend server.

        This is a convenience method that initializes the application
        (if not already initialized) and starts the server.
        """
        if not self._initialized:
            await self.initialize()

        if self.mcp_frontend:
            await self.mcp_frontend.start()
