from typing import Optional, List
from common.database import DBConfig, Db
from common.logging import get_logger, span

from common.models.tenant_seat import TenantSeat
from common.config.config import ProModelSettings

logger = get_logger(__name__)


class TenantSeatError(Exception):
    """Base exception for tenant seat operations."""


class TenantSeatValidationError(TenantSeatError):
    """Invalid payload or precondition for tenant seat operations."""


class TenantSeatNotFoundError(TenantSeatError):
    """Requested tenant seat subject was not found."""


class TenantSeatForbiddenError(TenantSeatError):
    """Tenant seat action is not permitted."""


class TenantSeatConflictError(TenantSeatError):
    """Tenant seat action conflicts with existing state."""


SELECT_BASE = """
    SELECT id, user_id, email, name, created_at, deleted_at
    FROM tenant_seats
"""

GET_USER_FULL_NAME_QUERY = """
    SELECT first_name, last_name
    FROM users
    WHERE id = %s
    LIMIT 1
"""

GET_USER_TWITCH_USERNAME_QUERY = """
    SELECT twitch_username
    FROM twitch_account
    WHERE user_id = %s
    LIMIT 1
"""

GET_USER_KICK_USERNAME_QUERY = """
    SELECT kick_username
    FROM kick_account
    WHERE user_id = %s
    LIMIT 1
"""

GET_USER_EMAIL_QUERY = """
    SELECT email
    FROM users
    WHERE id = %s
    LIMIT 1
"""

COUNT_ACTIVE_BY_USER_QUERY = """
    SELECT COUNT(*)
    FROM tenant_seats
    WHERE user_id = %s
      AND deleted_at IS NULL
"""

GET_ACTIVE_SEAT_NAME_ONLY_BY_EMAIL_QUERY = """
    SELECT name
    FROM tenant_seats
    WHERE email = %s
      AND deleted_at IS NULL
      AND name IS NOT NULL
      AND btrim(name) <> ''
    ORDER BY created_at DESC, id DESC
    LIMIT 1
"""

SELECT_ACTIVE_BY_USER_QUERY = (
    SELECT_BASE
    + """
    WHERE user_id = %s
      AND deleted_at IS NULL
    ORDER BY created_at DESC, id DESC
"""
)

SOFT_DELETE_BY_ID_QUERY = """
    UPDATE tenant_seats
    SET deleted_at = NOW()
    WHERE id = %s
      AND deleted_at IS NULL
"""

GET_BY_USER_AND_EMAIL_QUERY = (
    SELECT_BASE
    + """
    WHERE user_id = %s
      AND email = %s
    ORDER BY id DESC
    LIMIT 1
"""
)

UPSERT_REVIVE_QUERY = """
    INSERT INTO tenant_seats (user_id, email, name)
    VALUES (%s, %s, %s)
    ON CONFLICT (user_id, email)
    DO UPDATE SET
        name = EXCLUDED.name,
        deleted_at = NULL
    RETURNING id, user_id, email, name, created_at, deleted_at
"""

SELECT_BY_ID_QUERY = (
    SELECT_BASE
    + """
    WHERE id = %s
    LIMIT 1
"""
)


TENANT_SEAT_EXISTS_BY_EMAIL_QUERY = """
    SELECT 1
    FROM tenant_seats
    WHERE email = %s
      AND deleted_at IS NULL
    LIMIT 1
"""


class TenantSeatsDAO:
    """DAO for tenant_seats."""

    def __init__(self, db: Optional[Db] = None):
        self.db = db if db else Db(DBConfig(pg_dsn=None))
        self.proSettings = ProModelSettings()
        logger.debug(
            "TenantSeatsDAO initialized", extra={"component": "TenantSeatsDAO"}
        )

    async def count_by_user(self, user_id: int) -> int:
        """Return how many ACTIVE seats a user has (deleted_at IS NULL)."""
        with span(logger, "ts_count_by_user", {"user_id": user_id}):
            row = await self.db.fetch_one(COUNT_ACTIVE_BY_USER_QUERY, (user_id,))
            if row is None:
                raise RuntimeError(
                    f"Count query returned no row for tenant seats user_id={user_id}"
                )
            try:
                return int(row[0])
            except (TypeError, KeyError):
                raise

    async def get_by_user(self, user_id: int) -> List[TenantSeat]:
        """Return all ACTIVE seats for a user."""
        with span(logger, "ts_get_by_user", {"user_id": user_id}):
            rows = await self.db.fetch_all(SELECT_ACTIVE_BY_USER_QUERY, (user_id,))
            if rows is None:
                return []
            return [self._row_to_model(r) for r in rows]

    async def delete(self, seat_id: int) -> None:
        """Soft delete a seat by id."""
        with span(logger, "ts_delete", {"id": seat_id}):
            await self.db.execute_commit(SOFT_DELETE_BY_ID_QUERY, (seat_id,))
            logger.info("Soft-deleted tenant seat", extra={"id": seat_id})

    async def create(self, user_id: int, email: str, name: str) -> TenantSeat:
        """
        Create or revive seat using UNIQUE(user_id,email).
        - If exists active: updates name (doesn't consume a new seat)
        - If exists deleted or doesn't exist: enforces max_seats, then revives/inserts
        """
        with span(logger, "ts_create", {"user_id": user_id}):
            e = (email or "").strip().lower()
            n = (name or "").strip()

            if not e:
                raise TenantSeatValidationError("email cannot be empty")

            existing = await self.db.fetch_one(
                GET_BY_USER_AND_EMAIL_QUERY, (user_id, e)
            )

            # If seat doesn't exist OR exists but is soft-deleted, it will become ACTIVE => enforce limit
            if not existing or existing[5] is not None:  # deleted_at is at index 5
                current_active = await self.count_by_user(user_id)
                if current_active >= self.proSettings.max_seats:
                    raise TenantSeatConflictError(
                        f"User {user_id} exceeded max_seats ({self.proSettings.max_seats})"
                    )

            # Upsert: pass deleted_at=None so conflict update revives the seat
            columns = ["user_id", "email", "name", "deleted_at"]
            values = (user_id, e, n, None)

            upserted_id = await self.db.upsert(
                table="tenant_seats",
                columns=columns,
                values=values,
                conflict_columns=["user_id", "email"],
                update_columns=["name", "deleted_at"],  # revive + update name
            )

            if not upserted_id:
                raise RuntimeError("Upsert returned no id for tenant_seats")

            row = await self.db.fetch_one(SELECT_BY_ID_QUERY, (upserted_id,))
            if not row:
                raise RuntimeError(f"Could not fetch tenant_seats row id={upserted_id}")

            model = self._row_to_model(row)
            logger.info(
                "Created/revived tenant seat",
                extra={"id": model.id, "user_id": user_id, "email": model.email},
            )
            return model

    def _row_to_model(self, row) -> TenantSeat:
        try:
            return TenantSeat(
                id=row[0],
                user_id=row[1],
                email=row[2],
                name=row[3],
                created_at=row[4],
                deleted_at=row[5],
            )
        except (TypeError, KeyError):
            raise

    async def get_user_full_name(self, user_id: int) -> str:
        """Return best display name for a user: Twitch username, else Kick username, else 'first last'."""
        with span(logger, "ts_get_user_full_name", {"user_id": user_id}):
            row = await self.db.fetch_one(GET_USER_TWITCH_USERNAME_QUERY, (user_id,))
            twitch_username = (row[0] or "").strip() if row else ""
            if twitch_username:
                return twitch_username

            row = await self.db.fetch_one(GET_USER_KICK_USERNAME_QUERY, (user_id,))
            kick_username = (row[0] or "").strip() if row else ""
            if kick_username:
                return kick_username

            row = await self.db.fetch_one(GET_USER_FULL_NAME_QUERY, (user_id,))
            if not row:
                raise TenantSeatNotFoundError(f"User {user_id} not found")

            first = (row[0] or "").strip()
            last = (row[1] or "").strip()
            full = f"{first} {last}".strip()

            if not full:
                raise TenantSeatValidationError(f"User {user_id} has no name on record")

            return full

    async def update_seat_name(self, user_id: int, email: str, name: str) -> TenantSeat:
        """Update seat name for (user_id, email)."""
        with span(logger, "ts_update_seat_name", {"user_id": user_id}):
            e = (email or "").strip().lower()
            n = (name or "").strip()

            if not e:
                raise TenantSeatValidationError("email cannot be empty")
            if not n:
                raise TenantSeatValidationError("name cannot be empty")

            row = await self.db.fetch_one(GET_BY_USER_AND_EMAIL_QUERY, (user_id, e))
            if not row:
                raise TenantSeatNotFoundError("Seat not found")

            # Only allow updates for active seats
            if row[5] is not None:  # deleted_at index
                raise TenantSeatNotFoundError("Seat is deleted")

            seat_id = int(row[0])

            await self.db.update(
                table="tenant_seats",
                set_columns=["name"],
                set_values=[n],
                where_clause="id = %s AND deleted_at IS NULL",
                where_params=(seat_id,),
            )

            updated = await self.db.fetch_one(SELECT_BY_ID_QUERY, (seat_id,))
            if not updated:
                raise RuntimeError(f"Could not fetch tenant_seats row id={seat_id}")

            return self._row_to_model(updated)

    async def get_user_email(self, user_id: int) -> str:
        """Return the user's email from users table."""
        with span(logger, "ts_get_user_email", {"user_id": user_id}):
            row = await self.db.fetch_one(GET_USER_EMAIL_QUERY, (user_id,))
            if not row or not row[0]:
                raise TenantSeatNotFoundError(f"User {user_id} not found")
            return str(row[0]).strip().lower()

    async def get_seat_name_by_email(self, email: str) -> str:
        """Return tenant_seats.name for an active seat by email (non-empty)."""
        with span(logger, "ts_get_seat_name_by_email", {"email": email}):
            e = (email or "").strip().lower()
            if not e:
                raise TenantSeatValidationError("email cannot be empty")

            row = await self.db.fetch_one(
                GET_ACTIVE_SEAT_NAME_ONLY_BY_EMAIL_QUERY, (e,)
            )
            name = (row[0] or "").strip() if row else ""
            if not name:
                return ""

            return name

    async def seat_exists_by_email(self, email: str) -> bool:
        """Return True if an ACTIVE tenant seat exists for the given email."""
        with span(logger, "ts_seat_exists_by_email", {"email": email}):
            e = (email or "").strip().lower()
            if not e:
                raise TenantSeatValidationError("email cannot be empty")

            row = await self.db.fetch_one(TENANT_SEAT_EXISTS_BY_EMAIL_QUERY, (e,))
            return row is not None
