"""Telegram channel repository — typed CRUD with direct account FK.

Each channel belongs to exactly one Telegram account via ``account_id`` FK.
No junction table is used.
"""

from __future__ import annotations

import json
import logging
import uuid
from datetime import datetime, timezone
from typing import Any

from repo_core import BaseRepository, hook
from repo_core.domains.telegram.channel.model import TelegramChannel

logger = logging.getLogger(__name__)


class TelegramChannelRepository(BaseRepository[TelegramChannel]):
    """CRUD for ``telegram_channels`` with direct ``account_id`` FK.

    Writes to both PostgreSQL (primary) and Doris (warehouse) so that
    analytics dashboards can JOIN ``telegram_channels`` to resolve
    numeric ``peer_id`` values into human-readable ``title``/``username``.

    Usage::

        repo = TelegramChannelRepository(model=TelegramChannel)
        async with repo:
            channels, total = await repo.list_channels(limit=50, offset=0)
    """

    TABLE = "telegram_channels"
    _backends = {"db", "doris"}

    # ── Hooks ──────────────────────────────────────────────────────────────

    @hook("after_create")
    async def _sync_create_to_doris(self, channel: TelegramChannel) -> None:
        """After PostgreSQL insert, replicate to Doris warehouse."""
        logger.debug("Channel created: id=%s telegram_id=%s", channel.id, channel.channel_id)
        try:
            await self._warehouse_upsert(channel)
        except Exception:
            logger.warning(
                "Failed to sync new channel %s to Doris", channel.channel_id,
                exc_info=True,
            )

    @hook("after_update")
    async def _sync_update_to_doris(self, channel: TelegramChannel) -> None:
        """After PostgreSQL update, replicate to Doris warehouse."""
        logger.debug("Channel updated: id=%s", channel.id)
        try:
            await self._warehouse_upsert(channel)
        except Exception:
            logger.warning(
                "Failed to sync updated channel %s to Doris", channel.channel_id,
                exc_info=True,
            )

    @hook("after_delete")
    async def _sync_delete_from_doris(self, channel: TelegramChannel) -> None:
        """After PostgreSQL delete, remove from Doris warehouse."""
        logger.debug("Channel deleted: id=%s", channel.id)
        try:
            await self.warehouse_remove(
                self.TABLE, {"channel_id": channel.channel_id}
            )
        except Exception:
            logger.warning(
                "Failed to delete channel %s from Doris", channel.channel_id,
                exc_info=True,
            )

    async def _warehouse_upsert(self, channel: TelegramChannel) -> None:
        """Insert or update the channel row in the Doris warehouse.

        Only sends columns that actually exist in the Doris
        ``telegram_channels`` table (see migration
        ``telegram_channels_doris_0005``).  Extra model fields that
        are PostgreSQL-only are stripped to avoid ''Unknown column''
        errors.
        """
        data = channel.model_dump(mode="json")

        # ── Keep ONLY the columns that exist in the Doris schema ──────
        # NOTE: the actual Doris telegram_channels table does NOT have
        # an "id" column (the 0001_initial_setup consolidated migration
        # omitted it).  channel_id is the unique key.
        doris_columns = {
            "channel_id", "title", "username", "kind",
            "member_count", "description", "account_id",
            "assigned_at", "created_at", "updated_at",
        }
        data = {k: v for k, v in data.items() if k in doris_columns}

        # Serialize UUIDs and datetimes for Doris compatibility
        for key in ("account_id",):
            val = data.get(key)
            if val is not None and hasattr(val, "hex"):
                data[key] = str(val)
        for key in ("assigned_at", "created_at", "updated_at"):
            val = data.get(key)
            if val is not None and hasattr(val, "isoformat"):
                data[key] = val.isoformat()

        # Ensure the core display fields are never None / empty
        data.setdefault("title", str(data.get("channel_id", "")))
        data.setdefault("kind", "channel")

        existing = await self.warehouse_first(
            self.TABLE, {"channel_id": data["channel_id"]}
        )
        if existing:
            await self.warehouse_update(
                self.TABLE,
                {"channel_id": data["channel_id"]},
                data,
            )
            logger.debug(
                "Doris channel updated: channel_id=%s title=%s",
                data["channel_id"], data.get("title"),
            )
        else:
            # Use raw warehouse table insert — warehouse_insert_one would
            # try to build a TelegramChannel model from the result, but
            # the Doris row lacks "id" (not a column in this table).
            await self.warehouse_table(self.TABLE).create(data, returning=None)
            logger.info(
                "Doris channel inserted: channel_id=%s title=%s",
                data["channel_id"], data.get("title"),
            )

    # ── Warehouse sync (public) ──────────────────────────────────────────

    async def sync_to_warehouse(self, channel: TelegramChannel) -> None:
        """Public entry-point to upsert a channel into the Doris warehouse.

        Delegates to ``_warehouse_upsert``.  Use this for explicit /
        bulk backfill operations without going through the full CRUD
        hook cycle.
        """
        await self._warehouse_upsert(channel)

    # ── Channel CRUD ──────────────────────────────────────────────────────

    async def create_channel(self, data: dict[str, Any]) -> TelegramChannel:
        """Insert a new channel row."""
        data.setdefault("id", uuid.uuid4())
        await self.emit("before_create", data)
        created = await self.insert_one(self.TABLE, data, returning=["*"])
        await self.emit("after_create", created)
        return created

    async def upsert_channel(self, data: dict[str, Any]) -> TelegramChannel:
        """Insert or update a channel by its Telegram ``channel_id``.

        Uses the ``save`` method which upserts based on the id field.
        First looks up by ``channel_id`` to preserve the existing UUID
        if the channel already exists.

        Emits ``after_create`` for new channels and ``after_update`` for
        existing ones so that Doris hooks fire correctly.
        """
        existing = await self.get_by_channel_id(data["channel_id"])
        is_new = existing is None

        if not is_new:
            data["id"] = existing.id
            data.setdefault("title", existing.title)
            # Preserve existing account assignment unless explicitly overridden
            if "account_id" not in data and existing.account_id is not None:
                data["account_id"] = existing.account_id
        else:
            data.setdefault("id", uuid.uuid4())

        # Ensure non-nullable fields with defaults are never None
        data.setdefault("total_posts_analyzed", 0)
        data.setdefault("backfill_threshold", 30)

        if is_new:
            await self.emit("before_create", data)
            result = await self.save(self.TABLE, data, returning=["*"])
            await self.emit("after_create", result)
        else:
            await self.emit("before_save", data)
            result = await self.save(self.TABLE, data, returning=["*"])
            await self.emit("after_update", result)
        return result

    async def get_by_id(self, channel_id: uuid.UUID) -> TelegramChannel | None:
        """Get a channel by its primary key."""
        return await self.find_by_id(self.TABLE, channel_id)

    async def get_by_channel_id(self, channel_id: int) -> TelegramChannel | None:
        """Get a channel by its numeric Telegram channel ID."""
        return await self.first(self.TABLE, {"channel_id": channel_id})

    async def list_channels(
        self,
        *,
        kind: str | None = None,
        assigned: bool | None = None,
        search: str | None = None,
        account_id: uuid.UUID | None = None,
        limit: int = 50,
        offset: int = 0,
    ) -> tuple[list[dict[str, Any]], int]:
        """List channels with optional filters.

        Returns enriched dicts that include account info from the joined
        ``telegram_accounts`` table.
        """
        qb = self.table(self.TABLE)

        if kind is not None:
            qb = qb.where("kind", kind)
        if assigned is True:
            qb = qb.where_not_null("account_id")
        elif assigned is False:
            qb = qb.where_null("account_id")
        if account_id is not None:
            qb = qb.where("account_id", account_id)

        # Get all matching channels
        all_rows: list[dict[str, Any]] = await qb.order_by("-updated_at").all()

        enriched: list[dict[str, Any]] = []
        for row in all_rows:
            row_dict = dict(row)
            row_dict["assigned_accounts"] = []
            row_dict["assigned_count"] = 0

            # Enrich with account info if assigned
            acc_id = row_dict.get("account_id")
            if acc_id is not None:
                account = await self.table("telegram_accounts").where("id", acc_id).first()
                if account:
                    row_dict["assigned_accounts"] = [{
                        "account_id": str(acc_id),
                        "phone_number": account.get("phone_number", ""),
                        "username": account.get("username"),
                        "status": account.get("status", "unknown"),
                    }]
                    row_dict["assigned_count"] = 1

            # Search filter (case-insensitive)
            if search:
                s = search.lower().strip()
                title = str(row_dict.get("title", "")).lower()
                username = str(row_dict.get("username", "")).lower()
                cid = str(row_dict.get("channel_id", 0)).lower()
                account_match = any(
                    s in str(a.get("phone_number", "")).lower()
                    or s in str(a.get("username", "") or "").lower()
                    for a in row_dict["assigned_accounts"]
                )
                if s not in title and s not in username and s not in cid and not account_match:
                    continue

            enriched.append(row_dict)

        total = len(enriched)
        start = max(0, min(offset, total))
        end = min(start + max(1, limit), total)
        page = enriched[start:end]

        return page, total

    async def update_channel(
        self, channel_id: uuid.UUID, values: dict[str, Any],
    ) -> TelegramChannel | None:
        """Update channel fields by primary key."""
        # Serialize dict/list values to JSON strings for PostgreSQL JSONB columns
        for key, val in values.items():
            if isinstance(val, (dict, list)):
                values[key] = json.dumps(val)
        updated = await self.update_by_id(self.TABLE, channel_id, values)
        if updated is not None:
            await self.emit("after_update", updated)
        return updated

    async def delete_channel(self, channel_id: uuid.UUID) -> int:
        """Delete a channel by primary key."""
        channel = await self.get_by_id(channel_id)
        await self.emit("before_delete", channel_id)
        result = await self.remove_by_id(self.TABLE, channel_id)
        if channel is not None:
            await self.emit("after_delete", channel)
        return result

    # ── Assignment (direct FK update) ─────────────────────────────────────

    async def assign_channel(
        self,
        channel_db_id: uuid.UUID,
        account_id: uuid.UUID,
        *,
        assigned_by: str | None = None,
    ) -> dict[str, Any]:
        """Assign a channel to a single account by setting ``account_id`` FK.

        Validates the account exists and is in a valid status.
        Returns the updated channel record.
        """
        now = datetime.now(timezone.utc)

        # Validate account exists and check status
        account = await self.table("telegram_accounts").where("id", account_id).first()
        if account is None:
            raise ValueError(f"Account '{account_id}' not found")

        acc_status = account.get("status", "")
        if acc_status in ("banned", "deleted"):
            raise ValueError(f"Account status '{acc_status}' prevents assignment")

        # Check if this channel is already assigned to this same account
        channel = await self.get_by_id(channel_db_id)
        if channel is not None and channel.account_id == account_id:
            return {
                "channel_id": channel_db_id,
                "status": "already_assigned",
                "account_id": account_id,
            }

        # Set the FK
        updated = await self.update_channel(channel_db_id, {
            "account_id": account_id,
            "assigned_at": now,
            "assigned_by": assigned_by,
        })
        return {
            "channel_id": channel_db_id,
            "status": "assigned",
            "account_id": account_id,
            "channel": updated.model_dump() if updated and hasattr(updated, "model_dump") else None,
        }

    async def unassign_channel(
        self,
        channel_db_id: uuid.UUID,
    ) -> dict[str, Any]:
        """Unassign a channel by setting ``account_id`` to NULL."""
        updated = await self.update_channel(channel_db_id, {
            "account_id": None,
            "assigned_at": None,
            "assigned_by": None,
        })
        return {
            "channel_id": channel_db_id,
            "status": "unassigned",
            "channel": updated.model_dump() if updated and hasattr(updated, "model_dump") else None,
        }

    async def is_channel_assigned(self, channel_db_id: uuid.UUID) -> bool:
        """Check if a channel is currently assigned to any account."""
        channel = await self.get_by_id(channel_db_id)
        return channel is not None and channel.account_id is not None

    async def list_channels_for_account(
        self, account_id: uuid.UUID, *, limit: int = 50, offset: int = 0,
    ) -> tuple[list[dict[str, Any]], int]:
        """List all channels assigned to a specific account."""
        return await self.list_channels(
            account_id=account_id, limit=limit, offset=offset,
        )

    async def get_account_ids_with_channels(self) -> set[str]:
        """Return the set of account_id values that have >=1 channel assigned.

        Uses ``GROUP BY`` to emulate ``SELECT DISTINCT``, which is not
        natively supported by the query builder at this time.
        """
        rows = await self.table(self.TABLE).select("account_id").where_not_null("account_id").group_by("account_id").all()
        return {str(r["account_id"]) for r in rows if r.get("account_id")}
