from __future__ import annotations

import asyncio
import hashlib
import ipaddress
import json
import mimetypes
import os
import re
import sqlite3
import time
from pathlib import Path
from typing import Any
from urllib.parse import urlparse

import httpx
from fastapi import BackgroundTasks, Request
from fastapi.responses import JSONResponse

from media_runtime import VLM_SUPPORTED_IMAGE_MIME, image_mime, make_image_data_uri, normalize_image_for_vision


AVITO_API_BASE = "https://api.avito.ru"
TOKEN_ENDPOINT = "/token"
ACCOUNT_SELF_ENDPOINT = "/core/v1/accounts/self"
MESSAGES_ENDPOINT_TEMPLATE = "/messenger/v3/accounts/{user_id}/chats/{chat_id}/messages/"
SEND_MESSAGE_ENDPOINT_TEMPLATE = "/messenger/v1/accounts/{user_id}/chats/{chat_id}/messages"
UPLOAD_IMAGES_ENDPOINT_TEMPLATE = "/messenger/v1/accounts/{user_id}/uploadImages"
SEND_IMAGE_ENDPOINT_TEMPLATE = "/messenger/v1/accounts/{user_id}/chats/{chat_id}/messages/image"
CONNECT_TIMEOUT = 5.0
READ_TIMEOUT = 15.0
REQUEST_TIMEOUT = httpx.Timeout(timeout=READ_TIMEOUT, connect=CONNECT_TIMEOUT, read=READ_TIMEOUT, write=5.0, pool=5.0)
IMAGE_MAX_BYTES = 10 * 1024 * 1024
IMAGE_TIMEOUT = httpx.Timeout(timeout=20.0, connect=5.0, read=20.0, write=5.0, pool=5.0)
IMAGE_HOST_SUFFIXES = (".avito.ru", ".avito.st")
CARD_EXTENSIONS = (".jpg", ".jpeg", ".png", ".bmp", ".gif")
CARD_RE = re.compile(r"\[\s*(?:КАРТОЧКА)\s*:\s*([A-Za-z0-9_\-]+)\s*\]")
MARKDOWN_LINK_RE = re.compile(r"\[([^\]\n]+)\]\((https?://[^)\s]+)\)")

_TOKEN_CACHE = {"token": "", "token_type": "Bearer", "expires_at": 0.0, "account_id": ""}


def _channel_enabled() -> bool:
    return os.getenv("AVITO_ENABLED", "").lower() in {"1", "true", "yes", "on"}


def _route() -> str:
    route = os.getenv("AVITO_ROUTE", "direct").strip().lower()
    return route if route in {"direct", "bitrix", "amocrm"} else "direct"


def _enabled() -> bool:
    return _channel_enabled() and _route() == "direct"


def _root() -> Path:
    return Path(__file__).resolve().parent


def _db_path() -> Path:
    data_dir = os.getenv("DATA_DIR", "").strip()
    return Path(data_dir) / "lev_state.db" if data_dir else _root() / "lev_state.db"


def _connect() -> sqlite3.Connection:
    con = sqlite3.connect(_db_path(), timeout=30)
    con.execute("PRAGMA journal_mode=WAL")
    con.execute(
        "CREATE TABLE IF NOT EXISTS avito_events ("
        "provider TEXT NOT NULL DEFAULT 'avito', "
        "event_key TEXT NOT NULL UNIQUE, "
        "event_id TEXT, message_id TEXT, chat_id TEXT NOT NULL, "
        "received_at REAL NOT NULL, processing_state TEXT NOT NULL, "
        "response_message_id TEXT, error TEXT, updated_at REAL NOT NULL)"
    )
    _ensure_column(con, "text_state", "TEXT")
    _ensure_column(con, "media_state", "TEXT")
    _ensure_column(con, "media_slug", "TEXT")
    _ensure_column(con, "media_message_id", "TEXT")
    con.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_avito_events_message ON avito_events(message_id) WHERE message_id IS NOT NULL AND message_id != ''")
    con.commit()
    return con


def _ensure_column(con: sqlite3.Connection, name: str, decl: str) -> None:
    cols = {row[1] for row in con.execute("PRAGMA table_info(avito_events)")}
    if name not in cols:
        con.execute(f"ALTER TABLE avito_events ADD COLUMN {name} {decl}")


def _log(event: str, **kwargs: object) -> None:
    safe = []
    for key, value in kwargs.items():
        if key in {"text", "token", "secret", "authorization", "raw"}:
            continue
        val = str(value or "")
        if key in {"chat_id", "message_id", "event_id", "sender_id"} and val:
            val = "***" + val[-6:]
        safe.append(f"{key}={val}")
    print("AVITO: " + event + (" " + " ".join(safe) if safe else ""), flush=True)


def _nested_values(data: Any) -> list[dict[str, Any]]:
    values: list[dict[str, Any]] = []
    if isinstance(data, dict):
        values.append(data)
        for value in data.values():
            values.extend(_nested_values(value))
    elif isinstance(data, list):
        for item in data:
            values.extend(_nested_values(item))
    return values


def _first(data: dict[str, Any], keys: tuple[str, ...]) -> str:
    for obj in _nested_values(data):
        for key in keys:
            value = obj.get(key)
            if value not in (None, "") and not isinstance(value, (dict, list)):
                return str(value)
    return ""


def _text(data: dict[str, Any]) -> str:
    for obj in _nested_values(data):
        for key in ("text", "message", "body"):
            value = obj.get(key)
            if isinstance(value, str) and value.strip():
                return value
    return ""


def _extract_image_sizes(data: dict[str, Any]) -> dict[str, str]:
    content = data.get("content") if isinstance(data.get("content"), dict) else data
    image = content.get("image") if isinstance(content, dict) and isinstance(content.get("image"), dict) else {}
    sizes = image.get("sizes") if isinstance(image, dict) and isinstance(image.get("sizes"), dict) else {}
    result: dict[str, str] = {}
    for key, value in sizes.items():
        if isinstance(key, str) and isinstance(value, str) and value.strip():
            result[key] = value.strip()
    return result


def _select_image_variant(sizes: dict[str, str]) -> tuple[str, str]:
    parsed: list[tuple[int, str, str]] = []
    fallback: list[tuple[str, str]] = []
    for key, url in sizes.items():
        if not isinstance(key, str) or not isinstance(url, str) or not url.strip():
            continue
        match = re.fullmatch(r"(\d{1,5})x(\d{1,5})", key.strip())
        if match:
            width = int(match.group(1))
            height = int(match.group(2))
            if width > 0 and height > 0:
                parsed.append((width * height, key, url.strip()))
                continue
        fallback.append((key, url.strip()))
    if parsed:
        _, key, url = sorted(parsed, key=lambda item: (item[0], item[1]), reverse=True)[0]
        return key, url
    if fallback:
        key, url = sorted(fallback, key=lambda item: item[0])[0]
        return key, url
    raise RuntimeError("image_sizes_missing")


def _host_allowed(host: str) -> bool:
    clean = (host or "").strip().lower().rstrip(".")
    if not clean:
        return False
    try:
        ip = ipaddress.ip_address(clean)
    except ValueError:
        return clean == "avito.ru" or clean == "avito.st" or any(clean.endswith(suffix) for suffix in IMAGE_HOST_SUFFIXES)
    return ip.is_global


def _safe_image_url(url: str) -> bool:
    try:
        parsed = urlparse(str(url or ""))
    except Exception:
        return False
    if parsed.scheme.lower() != "https" or not parsed.hostname:
        return False
    if parsed.username or parsed.password:
        return False
    return _host_allowed(parsed.hostname)


async def _download_image_bytes(url: str) -> tuple[bytes, str, int, str]:
    if not _safe_image_url(url):
        raise RuntimeError("image_url_unsafe")
    http_content_type = ""
    async with httpx.AsyncClient(timeout=IMAGE_TIMEOUT, trust_env=False, follow_redirects=False) as client:
        async with client.stream("GET", url, headers={"Accept": "image/png,image/jpeg,image/gif,image/webp,*/*;q=0.8"}) as response:
            response.raise_for_status()
            http_content_type = str(response.headers.get("content-type") or "").split(";", 1)[0].strip().lower()
            chunks: list[bytes] = []
            total = 0
            async for chunk in response.aiter_bytes():
                if not chunk:
                    continue
                total += len(chunk)
                if total > IMAGE_MAX_BYTES:
                    raise RuntimeError("image_too_large")
                chunks.append(chunk)
    data = b"".join(chunks)
    mime = image_mime(data, http_content_type)
    return data, mime, len(data), http_content_type


def _normalize_markdown_links(text: str) -> str:
    return MARKDOWN_LINK_RE.sub(lambda m: m.group(2), text or "")


def _split_response_parts(text: str) -> list[dict[str, str]]:
    source = str(text or "")
    parts: list[dict[str, str]] = []
    pos = 0
    for match in CARD_RE.finditer(source):
        before = _normalize_markdown_links(source[pos:match.start()]).strip()
        if before:
            parts.append({"type": "text", "text": before})
        parts.append({"type": "card", "slug": match.group(1)})
        pos = match.end()
    tail = _normalize_markdown_links(source[pos:]).strip()
    if tail:
        parts.append({"type": "text", "text": tail})
    return parts


def _load_card_registry() -> set[str]:
    path = _root() / "card_views.json"
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return set()
    if not isinstance(data, dict):
        return set()
    return {str(key) for key in data.keys() if re.fullmatch(r"[A-Za-z0-9_\-]+", str(key))}


def _resolve_card_asset(slug: str) -> Path | None:
    clean = str(slug or "")
    if not re.fullmatch(r"[A-Za-z0-9_\-]+", clean):
        return None
    if clean not in _load_card_registry():
        return None
    cards_dir = (_root() / "cards").resolve()
    try:
        cards_dir.relative_to(_root().resolve())
    except ValueError:
        return None
    for ext in CARD_EXTENSIONS:
        candidate = (cards_dir / f"{clean}{ext}").resolve()
        try:
            candidate.relative_to(cards_dir)
        except ValueError:
            continue
        if candidate.is_file():
            return candidate
    return None


def _extract_message_id(data: Any) -> str:
    for obj in _nested_values(data):
        mid = obj.get("id") or obj.get("message_id")
        if mid:
            return str(mid)
    return ""


def _is_system(data: dict[str, Any]) -> bool:
    system = {"system", "service", "event", "notification", "deleted", "placeholder", "system_message", "service_message"}
    for obj in _nested_values(data):
        for key in ("kind", "message_type", "system_type", "event_type", "type"):
            if str(obj.get(key) or "").strip().lower() in system:
                return True
        for key in ("is_system", "system", "deleted"):
            if obj.get(key) is True:
                return True
    return False


def _extract_event(payload: dict[str, Any]) -> dict[str, str]:
    envelope = payload.get("payload")
    if isinstance(envelope, dict):
        value = envelope.get("value")
        if not isinstance(value, dict):
            return {"malformed": "invalid_webhook_envelope", "event_id": str(payload.get("id") or ""), "event_type": str(envelope.get("type") or "")}
        message_type = str(value.get("type") or "").strip()
        content = value.get("content") if isinstance(value.get("content"), dict) else {}
        text = str(content.get("text") or "") if message_type == "text" else ""
        image_sizes = _extract_image_sizes(value) if message_type == "image" else {}
        return {
            "schema": "v3",
            "event_id": str(payload.get("id") or ""),
            "event_type": str(envelope.get("type") or ""),
            "chat_id": str(value.get("chat_id") or ""),
            "chat_type": str(value.get("chat_type") or ""),
            "message_id": str(value.get("id") or ""),
            "sender_id": str(value.get("author_id") or ""),
            "author_id": str(value.get("author_id") or ""),
            "user_id": str(value.get("user_id") or ""),
            "timestamp": str(value.get("created") or payload.get("timestamp") or ""),
            "type": message_type,
            "text": text,
            "image_sizes_json": json.dumps(image_sizes, ensure_ascii=False, sort_keys=True, separators=(",", ":")) if image_sizes else "",
            "item_id": str(value.get("item_id") or ""),
        }
    return {
        "event_id": _first(payload, ("event_id", "eventId", "id")),
        "event_type": _first(payload, ("event_type", "eventType")),
        "chat_id": _first(payload, ("chat_id", "chatId", "chat", "dialog_id")),
        "message_id": _first(payload, ("message_id", "messageId", "message")),
        "sender_id": _first(payload, ("author_id", "user_id", "sender_id", "from_user_id")),
        "author_id": _first(payload, ("author_id", "sender_id", "from_user_id")),
        "user_id": _first(payload, ("account_user_id", "recipient_id", "owner_id")),
        "timestamp": _first(payload, ("created", "created_at", "timestamp")),
        "type": _first(payload, ("type", "message_type", "kind")),
        "text": _text(payload),
        "image_sizes_json": "",
    }


def _reserve(event: dict[str, str]) -> bool:
    event_key = event.get("event_id") or event.get("message_id")
    if not event_key or not event.get("chat_id"):
        return False
    now = time.time()
    con = _connect()
    try:
        cur = con.execute(
            "INSERT OR IGNORE INTO avito_events(event_key,event_id,message_id,chat_id,received_at,processing_state,updated_at) VALUES(?,?,?,?,?,?,?)",
            (event_key, event.get("event_id", ""), event.get("message_id", ""), event.get("chat_id", ""), now, "received", now),
        )
        con.commit()
        return cur.rowcount == 1
    finally:
        con.close()


def _mark(
    event: dict[str, str],
    state: str,
    *,
    response_message_id: str = "",
    error: str = "",
    text_state: str | None = None,
    media_state: str | None = None,
    media_slug: str | None = None,
    media_message_id: str | None = None,
) -> None:
    event_key = event.get("event_id") or event.get("message_id")
    if not event_key:
        return
    fields: list[str] = ["processing_state=?", "updated_at=?"]
    values: list[object] = [state, time.time()]
    if response_message_id:
        fields.append("response_message_id=?")
        values.append(response_message_id)
    if error:
        fields.append("error=?")
        values.append(str(error or "")[:180])
    if text_state is not None:
        fields.append("text_state=?")
        values.append(text_state)
    if media_state is not None:
        fields.append("media_state=?")
        values.append(media_state)
    if media_slug is not None:
        fields.append("media_slug=?")
        values.append(media_slug)
    if media_message_id is not None:
        fields.append("media_message_id=?")
        values.append(media_message_id)
    values.append(event_key)
    con = _connect()
    try:
        con.execute(f"UPDATE avito_events SET {', '.join(fields)} WHERE event_key=?", values)
        con.commit()
    finally:
        con.close()


async def _auth() -> tuple[str, str, str]:
    now = time.time()
    if _TOKEN_CACHE["token"] and float(_TOKEN_CACHE["expires_at"] or 0) > now + 60 and _TOKEN_CACHE["account_id"]:
        return str(_TOKEN_CACHE["token"]), str(_TOKEN_CACHE["token_type"] or "Bearer"), str(_TOKEN_CACHE["account_id"])
    client_id = os.getenv("AVITO_CLIENT_ID", "").strip()
    client_secret = os.getenv("AVITO_CLIENT_SECRET", "").strip()
    if not client_id or not client_secret:
        raise RuntimeError("avito_credentials_missing")
    async with httpx.AsyncClient(base_url=AVITO_API_BASE, timeout=REQUEST_TIMEOUT, trust_env=False) as client:
        token_response = await client.post(TOKEN_ENDPOINT, data={"grant_type": "client_credentials", "client_id": client_id, "client_secret": client_secret}, headers={"Accept": "application/json"})
        token_response.raise_for_status()
        token_data = token_response.json()
        token = str(token_data.get("access_token") or "")
        token_type = str(token_data.get("token_type") or "Bearer")
        expires_in = int(token_data.get("expires_in") or 3600)
        if not token:
            raise RuntimeError("avito_token_missing")
        headers = {"Authorization": f"{token_type} {token}", "Accept": "application/json"}
        account_response = await client.get(ACCOUNT_SELF_ENDPOINT, headers=headers)
        account_response.raise_for_status()
        account_data = account_response.json()
        account = account_data.get("result") if isinstance(account_data.get("result"), dict) else account_data
        account_id = str(account.get("id") or "") if isinstance(account, dict) else ""
        if not account_id:
            raise RuntimeError("avito_account_missing")
        _TOKEN_CACHE.update({"token": token, "token_type": token_type, "expires_at": now + max(120, expires_in - 60), "account_id": account_id})
        return token, token_type, account_id


async def _hydrate_message(event: dict[str, str], token: str, token_type: str, account_id: str) -> dict[str, str]:
    if (event.get("text") and event.get("type") != "image") or not event.get("chat_id"):
        return event
    headers = {"Authorization": f"{token_type} {token}", "Accept": "application/json"}
    async with httpx.AsyncClient(base_url=AVITO_API_BASE, timeout=REQUEST_TIMEOUT, trust_env=False) as client:
        response = await client.get(MESSAGES_ENDPOINT_TEMPLATE.format(user_id=account_id, chat_id=event["chat_id"]), headers=headers)
        response.raise_for_status()
        data = response.json()
    messages = data.get("messages") or data.get("items") or data.get("result") or data.get("data") or []
    if isinstance(messages, dict):
        messages = messages.get("messages") or messages.get("items") or []
    if not isinstance(messages, list):
        return event
    for msg in messages:
        if not isinstance(msg, dict):
            continue
        msg_id = _first(msg, ("id", "message_id", "messageId"))
        if event.get("message_id") and msg_id != event.get("message_id"):
            continue
        event["text"] = event.get("text") or _text(msg)
        event["sender_id"] = event.get("sender_id") or _first(msg, ("author_id", "user_id", "sender_id", "from_user_id"))
        msg_type = _first(msg, ("type", "message_type", "kind"))
        if msg_type:
            event["type"] = msg_type
        image_sizes = _extract_image_sizes(msg)
        if image_sizes:
            event["image_sizes_json"] = json.dumps(image_sizes, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
        break
    return event


async def _prepare_inbound_image(event: dict[str, str], token: str, token_type: str, account_id: str) -> str:
    if not event.get("image_sizes_json"):
        event = await _hydrate_message(event, token, token_type, account_id)
    try:
        sizes = json.loads(event.get("image_sizes_json") or "{}")
    except json.JSONDecodeError:
        sizes = {}
    if not isinstance(sizes, dict):
        sizes = {}
    variant, url = _select_image_variant({str(k): str(v) for k, v in sizes.items()})
    data, detected_mime, byte_len, http_content_type = await _download_image_bytes(url)
    try:
        normalized, normalized_mime, normalize_action = normalize_image_for_vision(data, detected_mime)
    except Exception as normalize_exc:
        _log(
            "image",
            chat_id=event.get("chat_id"),
            message_id=event.get("message_id"),
            variant=variant,
            http_content_type=http_content_type,
            detected_mime=detected_mime,
            bytes=byte_len,
            normalize="FAIL",
            normalized_mime="",
            normalized_bytes=0,
            vision_input="FAIL",
            reason=type(normalize_exc).__name__,
        )
        raise
    vision_input = "PASS" if normalized_mime in VLM_SUPPORTED_IMAGE_MIME else "FAIL"
    _log(
        "image",
        chat_id=event.get("chat_id"),
        message_id=event.get("message_id"),
        variant=variant,
        http_content_type=http_content_type,
        detected_mime=detected_mime,
        bytes=byte_len,
        normalize=normalize_action,
        normalized_mime=normalized_mime,
        normalized_bytes=len(normalized),
        vision_input=vision_input,
    )
    if normalized_mime not in VLM_SUPPORTED_IMAGE_MIME:
        raise RuntimeError("image_mime_unsupported")
    return make_image_data_uri(normalized, normalized_mime)


async def _post_with_retry(endpoint: str, headers: dict[str, str], *, json_payload: dict[str, Any] | None = None, files: dict[str, Any] | None = None) -> httpx.Response:
    last_error = ""
    async with httpx.AsyncClient(base_url=AVITO_API_BASE, timeout=REQUEST_TIMEOUT, trust_env=False) as client:
        for attempt in range(2):
            try:
                if files is not None:
                    response = await client.post(endpoint, headers=headers, files=files)
                else:
                    response = await client.post(endpoint, headers=headers, json=json_payload)
            except httpx.RequestError as exc:
                last_error = type(exc).__name__
                if attempt == 0:
                    await asyncio.sleep(0.5)
                    continue
                raise RuntimeError(last_error)
            if response.status_code in {200, 201, 202}:
                return response
            if response.status_code == 401 and attempt == 0:
                _TOKEN_CACHE.update({"token": "", "expires_at": 0.0, "account_id": ""})
                new_token, new_type, _ = await _auth()
                headers["Authorization"] = f"{new_type} {new_token}"
                continue
            if response.status_code == 429 and attempt == 0:
                try:
                    delay = min(float(response.headers.get("Retry-After") or "1"), 2.0)
                except ValueError:
                    delay = 1.0
                await asyncio.sleep(delay)
                continue
            if response.status_code >= 500 and attempt == 0:
                await asyncio.sleep(0.5)
                continue
            raise RuntimeError(f"send_http_{response.status_code}")
    raise RuntimeError(last_error or "send_failed")


async def _send_text(chat_id: str, text: str, token: str, token_type: str, account_id: str) -> str:
    headers = {"Authorization": f"{token_type} {token}", "Accept": "application/json"}
    payload = {"message": {"text": text}, "type": "text"}
    endpoint = SEND_MESSAGE_ENDPOINT_TEMPLATE.format(user_id=account_id, chat_id=chat_id)
    response = await _post_with_retry(endpoint, headers, json_payload=payload)
    try:
        return _extract_message_id(response.json())
    except ValueError:
        return ""


async def _upload_image(path: Path, token: str, token_type: str, account_id: str) -> str:
    endpoint = UPLOAD_IMAGES_ENDPOINT_TEMPLATE.format(user_id=account_id)
    mime = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
    headers = {"Authorization": f"{token_type} {token}", "Accept": "application/json"}
    with path.open("rb") as fh:
        response = await _post_with_retry(endpoint, headers, files={"uploadfile[]": (path.name, fh, mime)})
    data = response.json()
    if not isinstance(data, dict) or not data:
        raise RuntimeError("image_upload_empty")
    image_id = next(iter(data.keys()))
    if not image_id:
        raise RuntimeError("image_id_missing")
    return str(image_id)


async def _send_image(chat_id: str, image_id: str, token: str, token_type: str, account_id: str) -> str:
    headers = {"Authorization": f"{token_type} {token}", "Accept": "application/json"}
    endpoint = SEND_IMAGE_ENDPOINT_TEMPLATE.format(user_id=account_id, chat_id=chat_id)
    response = await _post_with_retry(endpoint, headers, json_payload={"image_id": image_id})
    try:
        return _extract_message_id(response.json())
    except ValueError:
        return ""


async def _process(server_module, event: dict[str, str]) -> None:
    _mark(event, "processing")
    try:
        token, token_type, account_id = await _auth()
        event = await _hydrate_message(event, token, token_type, account_id)
        if event.get("sender_id") and event.get("sender_id") == account_id:
            _mark(event, "responded", error="own_message_ignored")
            _log("own message ignored", chat_id=event.get("chat_id"), message_id=event.get("message_id"))
            return
        message_type = str(event.get("type") or "").lower()
        text = str(event.get("text") or "").strip()
        image_data_uri = ""
        if message_type == "image":
            try:
                image_data_uri = await _prepare_inbound_image(event, token, token_type, account_id)
            except Exception as image_exc:
                _mark(event, "failed", error=type(image_exc).__name__, media_state="failed")
                _log("image failed", chat_id=event.get("chat_id"), message_id=event.get("message_id"), error=type(image_exc).__name__)
                return
        if not text and not image_data_uri:
            _mark(event, "failed", error="text_missing")
            _log("text missing", chat_id=event.get("chat_id"), message_id=event.get("message_id"))
            return
        reply = await asyncio.wait_for(server_module.turn("avito:" + event["chat_id"], text, image_data_uri, "avito:" + event.get("sender_id", "")), timeout=150)
        parts = _split_response_parts(str(reply or "")) or [{"type": "text", "text": "\u0413\u043e\u0442\u043e\u0432\u043e."}]
        text_sent = False
        media_sent = False
        media_failed = False
        last_message_id = ""
        for part in parts:
            if part.get("type") == "text":
                clean_text = str(part.get("text") or "").strip()
                if not clean_text:
                    continue
                outgoing_id = await _send_text(event["chat_id"], clean_text, token, token_type, account_id)
                text_sent = True
                last_message_id = outgoing_id or last_message_id
                _mark(event, "processing", response_message_id=last_message_id, text_state="sent")
                _log("text sent", chat_id=event.get("chat_id"), message_id=event.get("message_id"), response_message_id=outgoing_id)
                continue
            if part.get("type") == "card":
                slug = str(part.get("slug") or "")
                path = _resolve_card_asset(slug)
                if not path:
                    _mark(event, "responded" if text_sent or media_sent else "failed", error="card_resolve_failed", media_state="failed", media_slug=slug)
                    _log("media", chat_id=event.get("chat_id"), message_id=event.get("message_id"), card=slug, resolve="failed")
                    continue
                try:
                    image_id = await _upload_image(path, token, token_type, account_id)
                    image_message_id = await _send_image(event["chat_id"], image_id, token, token_type, account_id)
                except Exception as media_exc:
                    _mark(event, "responded" if text_sent or media_sent else "failed", error=type(media_exc).__name__, media_state="failed", media_slug=slug)
                    _log("media", chat_id=event.get("chat_id"), message_id=event.get("message_id"), card=slug, asset="resolved", upload="failed", error=type(media_exc).__name__)
                    break
                media_sent = True
                last_message_id = image_message_id or last_message_id
                _mark(event, "processing", response_message_id=last_message_id, media_state="sent", media_slug=slug, media_message_id=image_message_id)
                _log("media", chat_id=event.get("chat_id"), message_id=event.get("message_id"), card=slug, asset="resolved", upload="ok", image="***" + image_id[-6:], send="ok")
        if text_sent or media_sent:
            final_media_state = "sent" if media_sent else ("failed" if media_failed else None)
            _mark(event, "responded", response_message_id=last_message_id, text_state="sent" if text_sent else None, media_state=final_media_state)
            _log("responded", chat_id=event.get("chat_id"), message_id=event.get("message_id"), response_message_id=last_message_id)
        else:
            _mark(event, "failed", error="nothing_sent")
            _log("failed", chat_id=event.get("chat_id"), message_id=event.get("message_id"), error="nothing_sent")
    except Exception as exc:
        _mark(event, "failed", error=type(exc).__name__)
        _log("failed", chat_id=event.get("chat_id"), message_id=event.get("message_id"), error=type(exc).__name__)


def install(server_module):
    app = server_module.app

    @app.get("/avito/webhook")
    async def levbot_avito_webhook_health():
        return {"ok": True, "provider": "avito", "enabled": _channel_enabled(), "route": _route(), "direct_enabled": _enabled()}

    @app.post("/avito/webhook")
    async def levbot_avito_webhook(request: Request, background_tasks: BackgroundTasks):
        if not _channel_enabled():
            return JSONResponse(status_code=403, content={"ok": False, "error": "avito_disabled"})
        if not _enabled():
            _log("direct route ignored", route=_route())
            return {"ok": True, "ignored": "route_not_direct", "route": _route()}
        try:
            payload = await request.json()
        except Exception:
            return JSONResponse(status_code=400, content={"ok": False, "error": "invalid_json"})
        if not isinstance(payload, dict):
            return JSONResponse(status_code=400, content={"ok": False, "error": "invalid_payload"})
        if _is_system(payload):
            _log("system event ignored")
            return {"ok": True, "ignored": "system"}
        event = _extract_event(payload)
        if event.get("malformed"):
            return JSONResponse(status_code=400, content={"ok": False, "error": event.get("malformed")})
        if event.get("schema") == "v3" and event.get("event_type") and event.get("event_type") != "message":
            _log("unsupported event ignored", event_id=event.get("event_id"), message_type=event.get("event_type"))
            return {"ok": True, "ignored": "unsupported"}
        if not event.get("chat_id") or not event.get("message_id"):
            return JSONResponse(status_code=400, content={"ok": False, "error": "missing_message_reference"})
        message_type = str(event.get("type") or "").lower()
        if message_type and message_type not in {"text", "image", "message", "messenger", ""}:
            _log("unsupported event ignored", chat_id=event.get("chat_id"), message_id=event.get("message_id"), event_id=event.get("event_id"), message_type=message_type)
            return {"ok": True, "ignored": "unsupported"}
        if event.get("schema") == "v3" and message_type == "text" and not str(event.get("text") or "").strip():
            return JSONResponse(status_code=400, content={"ok": False, "error": "missing_text"})
        if event.get("sender_id") and event.get("user_id") and event.get("sender_id") == event.get("user_id"):
            _log("own message ignored", chat_id=event.get("chat_id"), message_id=event.get("message_id"), sender_id=event.get("sender_id"))
            return {"ok": True, "ignored": "own"}
        if not _reserve(event):
            _log("duplicate skipped", chat_id=event.get("chat_id"), message_id=event.get("message_id"), event_id=event.get("event_id"))
            return {"ok": True, "duplicate": True}
        _log("event received", event_id=event.get("event_id"), chat_id=event.get("chat_id"), message_id=event.get("message_id"), message_type=message_type or "text", direction="inbound", dedup="new")
        background_tasks.add_task(_process, server_module, event)
        return {"ok": True}

    return app
