from __future__ import annotations

import json
import os
import re
import shlex
import subprocess
from collections.abc import Callable, Mapping
from pathlib import Path
from typing import Any

from external_connector.providers.fake import _session_view
from external_connector.state import SidecarStateStore


Runner = Callable[[list[str], str, float], tuple[int, str, str]]


def default_runner(command: list[str], cwd: str, timeout: float) -> tuple[int, str, str]:
    completed = subprocess.run(command, cwd=cwd, text=True, capture_output=True, check=False, timeout=timeout)
    return completed.returncode, completed.stdout, completed.stderr


class VendorCliProvider:
    provider_id = "vendor_cli"

    def __init__(
        self,
        *,
        store: SidecarStateStore,
        env: Mapping[str, str] | None = None,
        runner: Runner = default_runner,
    ) -> None:
        self.store = store
        self.env = env or os.environ
        self.runner = runner
        self.command_timeout_seconds = float(self.env.get("CONNECTOR_COMMAND_TIMEOUT_SECONDS") or 120)

    def connectors(self) -> list[dict[str, Any]]:
        return [
            {
                "kind": "weixin",
                "displayName": "Weixin",
                "authType": "qr",
                "providerId": self.provider_id,
                "capabilities": ["receive_text", "send_text", "receive_media", "direct_messages"],
            },
            {
                "kind": "feishu",
                "displayName": "Feishu/Lark",
                "authType": "plugin_install",
                "providerId": self.provider_id,
                "capabilities": ["receive_text", "send_text", "receive_media", "groups"],
            },
        ]

    def health(self) -> dict[str, Any]:
        return {"ok": True, "providerId": self.provider_id}

    def start_session(self, payload: dict[str, Any]) -> dict[str, Any]:
        kind = str(payload["kind"])
        session = self.store.create_session(
            kind=kind,
            connection_id=str(payload["connectionId"]),
            channel_id=str(payload["channelId"]),
            display_name=str(payload["displayName"]),
            options=dict(payload.get("options") or {}),
        )
        try:
            command_template = self._command_template(kind)
        except RuntimeError as exc:
            session = self.store.update_session(session.session_id, status="error", error=str(exc))
            return _session_view(session)

        connector_home = Path(self.store.path).parent
        state_dir = connector_home / kind / session.connection_id
        state_dir.mkdir(parents=True, exist_ok=True)
        command = shlex.split(command_template.format(state_dir=str(state_dir), connection_id=session.connection_id))
        try:
            code, stdout, stderr = self.runner(command, str(connector_home), self.command_timeout_seconds)
        except subprocess.TimeoutExpired:
            session = self.store.update_session(session.session_id, status="error", error="Provider command timed out")
            return _session_view(session)
        except Exception as exc:
            session = self.store.update_session(session.session_id, status="error", error=_redact(str(exc)))
            return _session_view(session)

        if code != 0:
            session = self.store.update_session(session.session_id, status="error", error=_redact(stderr or stdout))
            return _session_view(session)

        status = "connected" if "connected" in stdout.lower() else "waiting_for_user"
        account_id = _extract_account_id(stdout)
        session = self.store.update_session(
            session.session_id,
            status=status,
            account_id=account_id,
            metadata={"stateRef": str(state_dir)},
            instructions=["Complete the vendor install or verification flow"] if status != "connected" else [],
        )
        return _session_view(session)

    def get_session(self, session_id: str) -> dict[str, Any]:
        session = self.store.get_session(session_id)
        if session.status in {"cancelled", "error"}:
            return _session_view(session)
        command_template = self._optional_command_template(session.kind, "STATUS")
        if not command_template:
            return _session_view(session)
        connector_home = Path(self.store.path).parent
        state_dir = str(session.metadata.get("stateRef") or connector_home / session.kind / session.connection_id)
        command = shlex.split(
            command_template.format(
                state_dir=state_dir,
                connection_id=session.connection_id,
                session_id=session.session_id,
            )
        )
        try:
            code, stdout, stderr = self.runner(command, str(connector_home), self.command_timeout_seconds)
        except subprocess.TimeoutExpired:
            session = self.store.update_session(session.session_id, status="error", error="Provider status command timed out")
            return _session_view(session)
        except Exception as exc:
            session = self.store.update_session(session.session_id, status="error", error=_redact(str(exc)))
            return _session_view(session)
        if code != 0:
            session = self.store.update_session(session.session_id, status="error", error=_redact(stderr or stdout))
            return _session_view(session)
        updates = _provider_output_updates(stdout)
        if updates:
            metadata = dict(session.metadata)
            metadata.update(dict(updates.pop("metadata", {}) or {}))
            updates["metadata"] = metadata
            session = self.store.update_session(session.session_id, **updates)
        return _session_view(session)

    def cancel_session(self, session_id: str) -> None:
        self.store.update_session(session_id, status="cancelled")

    def logout(self, connection_id: str) -> None:
        return None

    def send(self, payload: dict[str, Any]) -> dict[str, Any]:
        begin = self.store.begin_send(connection_id=str(payload["connectionId"]), request_id=str(payload["requestId"]))
        if not begin.should_send:
            if begin.http_status == 409:
                return {
                    "ok": False,
                    "status": begin.status,
                    "retryAfterSeconds": begin.retry_after_seconds,
                    "httpStatus": 409,
                }
            return {"ok": True, "providerMessageId": begin.provider_message_id}
        command_template = self._optional_command_template(str(payload["kind"]), "SEND")
        if not command_template:
            provider_message_id = f"vendor_{payload['requestId']}"
            self.store.complete_send(begin.dedupe_key, provider_message_id=provider_message_id)
            return {"ok": True, "providerMessageId": provider_message_id}
        connector_home = Path(self.store.path).parent
        state_dir = connector_home / str(payload["kind"]) / str(payload["connectionId"])
        payload_path = _write_send_payload(state_dir, str(payload["requestId"]), payload)
        command = shlex.split(
            command_template.format(
                state_dir=str(state_dir),
                payload_path=str(payload_path),
                connection_id=str(payload["connectionId"]),
                channel_id=str(payload["channelId"]),
                request_id=str(payload["requestId"]),
                kind=str(payload["kind"]),
            )
        )
        try:
            code, stdout, stderr = self.runner(command, str(connector_home), self.command_timeout_seconds)
        except subprocess.TimeoutExpired:
            return {"ok": False, "error": "Provider send command timed out"}
        except Exception as exc:
            return {"ok": False, "error": _redact(str(exc))}
        if code != 0:
            return {"ok": False, "error": _redact(stderr or stdout)}
        provider_message_id = _extract_provider_message_id(stdout) or f"vendor_{payload['requestId']}"
        self.store.complete_send(begin.dedupe_key, provider_message_id=provider_message_id)
        return {"ok": True, "providerMessageId": provider_message_id}

    def _command_template(self, kind: str) -> str:
        key = "WEIXIN_CONNECT_COMMAND" if kind == "weixin" else "FEISHU_CONNECT_COMMAND"
        command = str(self.env.get(key) or "").strip()
        if not command:
            raise RuntimeError(f"{key} is required")
        return command

    def _optional_command_template(self, kind: str, action: str) -> str | None:
        prefix = "WEIXIN" if kind == "weixin" else "FEISHU"
        command = str(self.env.get(f"{prefix}_{action}_COMMAND") or "").strip()
        return command or None


def _extract_account_id(output: str) -> str | None:
    for part in output.split():
        if part.startswith("account="):
            return part.split("=", 1)[1]
    return None


def _provider_output_updates(output: str) -> dict[str, Any]:
    text = output.strip()
    if not text:
        return {}
    parsed: dict[str, Any] = {}
    try:
        raw = json.loads(text)
    except json.JSONDecodeError:
        raw = _parse_key_value_output(text)
    if isinstance(raw, dict):
        parsed = raw
    updates: dict[str, Any] = {}
    key_map = {
        "status": "status",
        "qrImage": "qr_image",
        "qr_image": "qr_image",
        "qrCode": "qr_code",
        "qr_code": "qr_code",
        "accountId": "account_id",
        "account_id": "account_id",
        "account": "account_id",
        "displayName": "display_name",
        "display_name": "display_name",
        "error": "error",
    }
    for source_key, target_key in key_map.items():
        value = parsed.get(source_key)
        if value is not None:
            updates[target_key] = str(value)
    instructions = parsed.get("instructions")
    if isinstance(instructions, list):
        updates["instructions"] = [str(item) for item in instructions]
    elif isinstance(instructions, str) and instructions.strip():
        updates["instructions"] = [instructions.strip()]
    metadata = parsed.get("metadata")
    if isinstance(metadata, dict):
        updates["metadata"] = metadata
    return updates


def _parse_key_value_output(text: str) -> dict[str, str]:
    values: dict[str, str] = {}
    for part in text.split():
        if "=" not in part:
            continue
        key, value = part.split("=", 1)
        if key:
            values[key] = value
    return values


def _write_send_payload(state_dir: Path, request_id: str, payload: dict[str, Any]) -> Path:
    sends_dir = state_dir / "sends"
    sends_dir.mkdir(parents=True, exist_ok=True)
    safe_request_id = re.sub(r"[^A-Za-z0-9_.-]+", "_", request_id) or "request"
    payload_path = sends_dir / f"{safe_request_id}.json"
    payload_path.write_text(json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n", encoding="utf-8")
    return payload_path


def _extract_provider_message_id(output: str) -> str | None:
    text = output.strip()
    if not text:
        return None
    try:
        raw = json.loads(text)
    except json.JSONDecodeError:
        raw = _parse_key_value_output(text)
    if not isinstance(raw, dict):
        return None
    value = raw.get("providerMessageId") or raw.get("provider_message_id") or raw.get("messageId") or raw.get("message_id")
    return str(value) if value is not None else None


def _redact(text: str) -> str:
    redacted = re.sub(r"\bsecret-[A-Za-z0-9._:-]+\b", "***", text)
    return re.sub(r"\b(appSecret|token|secret)=\S+", r"\1=***", redacted)
