"""
DVGatewayClient — Main SDK Entry Point

Wires together all internal components and exposes a clean,
type-safe public API for integrating with the DVGateway.

Example:
    gw = DVGatewayClient(
        base_url="https://gateway.example.com",
        auth=ApiKeyAuth(api_key=os.environ["DV_API_KEY"]),
    )

    # High-level: 5-line AI voice bot
    await (
        gw.pipeline()
        .stt(DeepgramAdapter(api_key="..."))
        .llm(AnthropicAdapter(api_key="..."))
        .tts(ElevenLabsAdapter(api_key="..."))
        .start()
    )
"""

from __future__ import annotations

from typing import AsyncIterable, Awaitable, Callable, Literal
from urllib.parse import quote as _url_quote

from dvgateway.auth.manager import AuthManager
from dvgateway.minutes.manager import MinutesManager
from dvgateway.observability.logger import create_logger
from dvgateway.observability.metrics import PipelineMetrics
from dvgateway.pipeline.builder import PipelineBuilder
from dvgateway.session.manager import SessionManager
from dvgateway.streams.audio_stream import AudioStream, create_audio_stream
from dvgateway.streams.call_events import CallEventStream
from dvgateway.streams.tts_stream import TtsInjector
from dvgateway.transport.http_client import HttpClient
from dvgateway.transport.ws_pool import WsPool
from dvgateway.types import (
    AudioChunk,
    CallEvent,
    CallSession,
    DVGatewayOptions,
    Logger,
    StreamDir,
    TranscriptResult,
    TtsAdapter,
    Unsubscribe,
)


class DVGatewayClient:
    """Main SDK entry point for the DVGateway real-time voice media gateway."""

    def __init__(
        self,
        base_url: str = "http://localhost:8080",
        auth: dict[str, str] | None = None,
        tenant_id: str | None = None,
        timeout: int = 30_000,
        reconnect: dict[str, object] | None = None,
        security: dict[str, object] | None = None,
        logger: Logger | None = None,
        *,
        options: DVGatewayOptions | None = None,
    ) -> None:
        # Support both dict-style and DVGatewayOptions
        if options:
            opts = options
        else:
            from dvgateway.types import ApiKeyAuth, JwtAuth, ReconnectOptions, SecurityOptions

            # Parse auth
            auth_dict = auth or {"type": "apiKey", "api_key": "dev-no-auth"}
            if auth_dict.get("type") == "jwt":
                parsed_auth = JwtAuth(token=auth_dict.get("token", ""))
            else:
                parsed_auth = ApiKeyAuth(api_key=auth_dict.get("api_key", "dev-no-auth"))

            # Parse reconnect
            recon_dict = reconnect or {}
            parsed_reconnect = ReconnectOptions(
                max_attempts=int(recon_dict.get("max_attempts", recon_dict.get("maxAttempts", 10))),  # type: ignore[arg-type]
                initial_delay_ms=int(recon_dict.get("initial_delay_ms", recon_dict.get("initialDelayMs", 2000))),  # type: ignore[arg-type]
                on_reconnect=recon_dict.get("on_reconnect", recon_dict.get("onReconnect")),  # type: ignore[arg-type]
            )

            # Parse security
            sec_dict = security or {}
            parsed_security = SecurityOptions(
                mask_pii=bool(sec_dict.get("mask_pii", sec_dict.get("maskPii", True))),
                force_tls=bool(sec_dict.get("force_tls", sec_dict.get("forceTls", True))),
            )

            opts = DVGatewayOptions(
                base_url=base_url,
                auth=parsed_auth,
                tenant_id=tenant_id,
                timeout=timeout,
                reconnect=parsed_reconnect,
                security=parsed_security,
                logger=logger,
            )

        # Enforce TLS in production
        normalized_url = self._normalize_base_url(opts)

        self._logger: Logger = opts.logger or create_logger(
            level="info",
            redact_pii=opts.security.mask_pii,
        )

        self._auth = AuthManager(opts.auth, normalized_url, self._logger)
        self._ws_pool = WsPool(self._auth, opts.reconnect, self._logger)
        self._http = HttpClient(self._auth, opts.timeout, self._logger, opts.tenant_id)
        self._call_events = CallEventStream(self._ws_pool, self._logger)
        self._tts_injector = TtsInjector(self._ws_pool, self._http, self._logger)
        self._sessions = SessionManager(self._http, self._logger)
        self._minutes = MinutesManager(self._http, self._logger)
        self.metrics = PipelineMetrics(self._logger)

    # ─── High-Level: Pipeline Builder ──────────────────────────────────────

    def pipeline(self) -> PipelineBuilder:
        """
        Create a fluent AI voice pipeline.

        Example:
            await (
                gw.pipeline()
                .stt(DeepgramAdapter(api_key="..."))
                .llm(AnthropicAdapter(api_key="..."))
                .tts(ElevenLabsAdapter(api_key="..."))
                .on_new_call(lambda s: print(f"Call: {s.linked_id}"))
                .start()
            )
        """
        return PipelineBuilder(self._ws_pool, self._call_events, self._tts_injector, self._logger)

    # ─── Mid-Level: Audio Streaming ─────────────────────────────────────────

    def stream_audio(
        self,
        linked_id: str,
        dir: StreamDir = "both",
    ) -> AudioStream:
        """
        Subscribe to real-time audio from a call.
        Returns an AsyncIterable that yields AudioChunk (Float32, 16kHz).
        """
        return create_audio_stream(self._ws_pool, linked_id, self._logger, dir)

    # ─── Mid-Level: TTS Injection ───────────────────────────────────────────

    async def inject_tts(self, linked_id: str, audio_chunks: AsyncIterable[bytes]) -> None:
        """Inject synthesized speech into an active call."""
        await self._tts_injector.inject_streaming(linked_id, audio_chunks)

    async def broadcast_tts(self, conf_id: str, audio_chunks: AsyncIterable[bytes]) -> None:
        """Broadcast synthesized speech to all conference participants."""
        await self._tts_injector.broadcast_to_conference(conf_id, audio_chunks)

    async def say(self, linked_id: str, text: str, tts: TtsAdapter) -> None:
        """
        Convenience: synthesize text and inject it directly.

        Tip: Pass a CachedTtsAdapter to avoid redundant API calls
        for repeated announcements (e.g. IVR greetings).

        Example::

            from dvgateway.adapters.tts import ElevenLabsAdapter, CachedTtsAdapter

            tts = CachedTtsAdapter(
                ElevenLabsAdapter(api_key="..."),
                provider="elevenlabs", cache_dir="./tts-cache",
            )
            await tts.warmup([{"text": "안녕하세요"}])
            await gw.say(linked_id, "안녕하세요", tts)  # cache hit
        """
        await self._tts_injector.inject_streaming(linked_id, tts.synthesize(text))

    async def broadcast_say(self, conf_id: str, text: str, tts: TtsAdapter) -> None:
        """Broadcast synthesized speech to a conference.

        Pass a CachedTtsAdapter for zero-cost repeated broadcasts.
        """
        await self._tts_injector.broadcast_to_conference(conf_id, tts.synthesize(text))

    # ─── Comfort Noise / Thinking Signals ─────────────────────────────────

    async def start_thinking(self, linked_id: str) -> None:
        """
        Notify the gateway that AI processing has started (STT → LLM → TTS).
        If comfort noise is enabled (GW_COMFORT_NOISE_ENABLED=true),
        low-level background audio will be injected into the call to prevent dead air.

        Can also be sent via the audio stream WebSocket:
            await audio_stream.send_thinking_start()

        Or via REST API:
            POST /api/v1/comfort/{linkedId}/start
        """
        await self._http.post(f"/api/v1/comfort/{_url_quote(linked_id, safe='')}/start", {})
        self._logger.debug({"linked_id": linked_id}, "Thinking signal: start")

    async def stop_thinking(self, linked_id: str) -> None:
        """
        Notify the gateway that AI processing has completed.
        Comfort noise injection will fade out and stop.
        """
        await self._http.post(f"/api/v1/comfort/{_url_quote(linked_id, safe='')}/stop", {})
        self._logger.debug({"linked_id": linked_id}, "Thinking signal: stop")

    # ─── Call Control ──────────────────────────────────────────────────────

    async def hangup(self, linked_id: str) -> None:
        """Terminate an active call by linkedId.

        Sends AMI Hangup action to Asterisk via the gateway.
        """
        await self._http.post(f"/api/v1/calls/{_url_quote(linked_id, safe='')}/hangup", {})
        self._logger.debug({"linked_id": linked_id}, "Call hangup requested")

    async def redirect(
        self,
        linked_id: str,
        destination: str,
        context: str = "from-internal",
    ) -> None:
        """Transfer (redirect) an active call to a different extension.

        Sends AMI Redirect action to Asterisk via the gateway.
        """
        await self._http.post(f"/api/v1/calls/{_url_quote(linked_id, safe='')}/redirect", {
            "context": context,
            "exten": destination,
            "priority": 1,
        })
        self._logger.debug(
            {"linked_id": linked_id, "destination": destination, "context": context},
            "Call redirect requested",
        )

    # ─── PBX Management ─────────────────────────────────────────────────

    async def apply_changes(self) -> dict:
        """Apply PBX configuration changes (required after callerID/extension changes)."""
        res = await self._http.post("/api/v1/pbx/apply-changes", {})
        return res.data

    async def click_to_call(
        self,
        caller: str,
        callee: str,
        cid_name: str = "",
        cid_number: str = "",
        account_code: str = "",
        custom_value_1: str = "",
        custom_value_2: str = "",
        custom_value_3: str = "",
    ) -> dict:
        """Initiate an outbound call via PBX click-to-call API."""
        body = {"caller": caller, "callee": callee}
        if cid_name:
            body["cidName"] = cid_name
        if cid_number:
            body["cidNumber"] = cid_number
        if account_code:
            body["accountCode"] = account_code
        if custom_value_1:
            body["customValue1"] = custom_value_1
        if custom_value_2:
            body["customValue2"] = custom_value_2
        if custom_value_3:
            body["customValue3"] = custom_value_3
        res = await self._http.post("/api/v1/pbx/click-to-call", body)
        self._logger.debug({"caller": caller, "callee": callee}, "Click-to-call requested")
        return res.data

    # ─── Call Forwarding (Diversions) ───────────────────────────────────

    async def get_diversions(self, extension: str, tenant_id: str = "") -> dict:
        """Get all forwarding rules for an extension."""
        query = f"?tenantId={_url_quote(tenant_id, safe='')}" if tenant_id else ""
        res = await self._http.get(f"/api/v1/diversions/{_url_quote(extension, safe='')}{query}")
        return res.data

    async def set_diversion(
        self,
        extension: str,
        cf_type: str,
        enable: str = "",
        destination: str = "",
        time_group: str = "",
        tenant_id: str = "",
    ) -> dict:
        """Set a forwarding rule (CFI/CFB/CFN/CFU)."""
        body = {}
        if enable:
            body["enable"] = enable
        if destination:
            body["destination"] = destination
        if time_group:
            body["timeGroup"] = time_group
        query = f"?tenantId={_url_quote(tenant_id, safe='')}" if tenant_id else ""
        res = await self._http.put(
            f"/api/v1/diversions/{_url_quote(extension, safe='')}/{cf_type}{query}", body
        )
        return res.data

    async def delete_diversion(self, extension: str, cf_type: str, tenant_id: str = "") -> dict:
        """Delete (disable + clear) a forwarding rule."""
        query = f"?tenantId={_url_quote(tenant_id, safe='')}" if tenant_id else ""
        res = await self._http.delete(
            f"/api/v1/diversions/{_url_quote(extension, safe='')}/{cf_type}{query}"
        )
        return res.data

    # ─── Caller ID ──────────────────────────────────────────────────────

    async def get_caller_id(self, extension: str) -> dict:
        """Get caller ID display for an extension."""
        res = await self._http.get(f"/api/v1/callerid/{_url_quote(extension, safe='')}")
        return res.data

    async def set_caller_id(
        self,
        extension: str,
        name: str = "",
        number: str = "",
        apply_changes: bool = False,
    ) -> dict:
        """Update external caller ID display (optionally auto-apply changes)."""
        body = {}
        if name:
            body["name"] = name
        if number:
            body["number"] = number
        if apply_changes:
            body["applyChanges"] = True
        res = await self._http.put(f"/api/v1/callerid/{_url_quote(extension, safe='')}", body)
        return res.data

    # ─── Early Media ────────────────────────────────────────────────────

    async def get_early_media(self, extension: str, tenant_id: str = "") -> dict:
        """Get early media config for an extension."""
        query = f"?tenantId={_url_quote(tenant_id, safe='')}" if tenant_id else ""
        res = await self._http.get(f"/api/v1/earlymedia/{_url_quote(extension, safe='')}{query}")
        return res.data

    async def set_early_media(
        self,
        extension: str,
        enabled: str = "",
        audio_url: str = "",
        tenant_id: str = "",
    ) -> dict:
        """Set early media config. When audio_url is provided, the file is
        downloaded and converted to Asterisk-compatible WAV automatically.

        Args:
            extension: Extension number (e.g. "07045144801")
            enabled: "yes" or "no"
            audio_url: Remote audio URL (mp3/wav/ogg — auto-converted to 8kHz WAV)
            tenant_id: Tenant ID (auto from JWT if omitted)
        """
        body = {}
        if enabled:
            body["enabled"] = enabled
        if audio_url:
            body["audioUrl"] = audio_url
        query = f"?tenantId={_url_quote(tenant_id, safe='')}" if tenant_id else ""
        res = await self._http.put(f"/api/v1/earlymedia/{_url_quote(extension, safe='')}{query}", body)
        return res.data

    # ─── Outbound Campaigns ─────────────────────────────────────────────

    async def list_campaigns(self) -> dict:
        """List all outbound call campaigns."""
        res = await self._http.get("/api/v1/pbx/campaigns")
        return res.data

    async def create_campaign(self, campaign: dict) -> dict:
        """Create a new campaign (scheduled, bulk, or recurring)."""
        res = await self._http.post("/api/v1/pbx/campaigns", campaign)
        return res.data

    async def get_campaign(self, campaign_id: str) -> dict:
        """Get campaign details."""
        res = await self._http.get(f"/api/v1/pbx/campaigns/{_url_quote(campaign_id, safe='')}")
        return res.data

    async def update_campaign(self, campaign_id: str, updates: dict) -> dict:
        """Update a campaign."""
        res = await self._http.put(
            f"/api/v1/pbx/campaigns/{_url_quote(campaign_id, safe='')}", updates
        )
        return res.data

    async def delete_campaign(self, campaign_id: str) -> dict:
        """Delete a campaign."""
        res = await self._http.delete(f"/api/v1/pbx/campaigns/{_url_quote(campaign_id, safe='')}")
        return res.data

    async def start_campaign(self, campaign_id: str) -> dict:
        """Start campaign execution."""
        res = await self._http.post(
            f"/api/v1/pbx/campaigns/{_url_quote(campaign_id, safe='')}/start", {}
        )
        return res.data

    async def pause_campaign(self, campaign_id: str) -> dict:
        """Pause a running campaign."""
        res = await self._http.post(
            f"/api/v1/pbx/campaigns/{_url_quote(campaign_id, safe='')}/pause", {}
        )
        return res.data

    async def resume_campaign(self, campaign_id: str) -> dict:
        """Resume a paused campaign."""
        res = await self._http.post(
            f"/api/v1/pbx/campaigns/{_url_quote(campaign_id, safe='')}/resume", {}
        )
        return res.data

    async def cancel_campaign(self, campaign_id: str) -> dict:
        """Cancel a running campaign."""
        res = await self._http.post(
            f"/api/v1/pbx/campaigns/{_url_quote(campaign_id, safe='')}/cancel", {}
        )
        return res.data

    async def get_campaign_results(self, campaign_id: str) -> dict:
        """Get campaign call results."""
        res = await self._http.get(
            f"/api/v1/pbx/campaigns/{_url_quote(campaign_id, safe='')}/results"
        )
        return res.data

    # ─── Mid-Level: Call Events ─────────────────────────────────────────────

    def on_call_event(
        self,
        handler: Callable[[CallEvent], None | Awaitable[None]],
    ) -> Unsubscribe:
        """Subscribe to all call lifecycle events. Returns an unsubscribe function."""
        return self._call_events.on(handler)

    def on(
        self,
        event_type: str,
        handler: Callable[..., None | Awaitable[None]],
    ) -> Unsubscribe:
        """Subscribe to a specific call event type."""
        return self._call_events.on_type(event_type, handler)

    # ─── Session Management ─────────────────────────────────────────────────

    async def list_sessions(self) -> list[CallSession]:
        """List all currently active call sessions."""
        return await self._sessions.list_sessions()

    async def list_sessions_by_tenant(self, tenant_id: str) -> list[CallSession]:
        """List active call sessions filtered by tenant ID."""
        return await self._sessions.list_sessions_by_tenant(tenant_id)

    async def update_session_meta(self, linked_id: str, meta: dict[str, str]) -> None:
        """Update metadata on a session."""
        await self._sessions.update_metadata(linked_id, meta)

    # ─── Minutes / Transcripts ──────────────────────────────────────────────

    async def submit_transcript(self, conf_id: str, result: TranscriptResult) -> None:
        """Submit an STT transcript result to the gateway's minutes store."""
        await self._minutes.submit_transcript(conf_id, result)

    async def download_minutes(
        self,
        conf_id: str,
        format: Literal["json", "txt"] = "txt",
    ) -> str:
        """Download conference minutes in JSON or plain-text format."""
        return await self._minutes.download_minutes(conf_id, format)

    def auto_submit_transcripts(
        self,
        conf_id: str,
    ) -> Callable[[TranscriptResult], Awaitable[None]]:
        """Returns a function that auto-submits final transcripts to minutes."""
        return self._minutes.create_auto_submitter(conf_id)

    # ─── Lifecycle ──────────────────────────────────────────────────────────

    def close(self) -> None:
        """Gracefully close all WebSocket connections and release resources."""
        self._ws_pool.close_all()
        self._logger.info({}, "DVGatewayClient closed")

    # ─── Private helpers ────────────────────────────────────────────────────

    def _normalize_base_url(self, opts: DVGatewayOptions) -> str:
        url = opts.base_url.rstrip("/")
        if opts.security.force_tls:
            if url.startswith("http://") and not self._should_skip_tls(url):
                self._logger: Logger = opts.logger or create_logger(level="info")  # type: ignore[assignment]
                url = url.replace("http://", "https://", 1)
        return url

    @staticmethod
    def _should_skip_tls(url: str) -> bool:
        """Return True if force_tls should NOT upgrade this URL to https://.

        TLS upgrade is skipped for:
          - IP addresses (any): TLS certificates are domain-based. Connecting
            to an IP address (public or private) via HTTPS almost never works
            because there's no valid certificate for raw IP addresses.
            Users who connect by IP always mean plain HTTP.
          - Loopback hostnames: localhost, *.local
        """
        from ipaddress import ip_address
        from urllib.parse import urlparse

        host = urlparse(url).hostname or ""

        # Loopback hostnames
        if host in ("localhost", "::1") or host.endswith(".local"):
            return True

        # Any IP address (public or private) — TLS certs don't cover raw IPs
        try:
            ip_address(host)
            return True  # valid IP → skip TLS
        except ValueError:
            return False  # not an IP → domain name → force_tls applies
