"""Marketplace module models.

Implements three tables of the approved Marketplace database design
(``docs/02_Architecture/Database/02_Marketplace.md``):

* :class:`Category` (``categories``)
* :class:`Listing` (``listings``)
* :class:`ListingImage` (``listing_images``)

The ``favorites`` table is intentionally deferred. All documented columns are
present (including those for not-yet-built features such as moderation, featured
listings and counters) so the schema matches the approved design exactly; the
MVP simply does not surface or mutate them.
"""

from __future__ import annotations

from app.extensions import db
from app.models.base import BaseModel, BigIntegerType, utcnow
from app.models.enums import (
    ListingCondition,
    ListingStatus,
    ListingVisibility,
    ReviewStatus,
)
from app.models.mixins import CreatedAtMixin, PublicIdMixin, TimestampMixin


def _enum_values(enum_cls):
    """Persist the ENUM member *values* (e.g. ``"good"``) rather than names."""
    return [member.value for member in enum_cls]


class Category(BaseModel, PublicIdMixin, TimestampMixin):
    """A marketplace category that groups listings."""

    __tablename__ = "categories"

    name = db.Column(db.String(100), nullable=False, unique=True)
    description = db.Column(db.Text, nullable=True)
    icon = db.Column(db.String(255), nullable=True)
    is_active = db.Column(db.Boolean, nullable=False, default=True, index=True)
    display_order = db.Column(db.Integer, nullable=False, default=1, index=True)

    def __repr__(self) -> str:
        return f"<Category id={self.id} name={self.name!r}>"


class Listing(BaseModel, PublicIdMixin, TimestampMixin):
    """A product listed for sale by a student."""

    __tablename__ = "listings"
    __table_args__ = (
        # created_at comes from TimestampMixin; the schema indexes it.
        db.Index("ix_listings_created_at", "created_at"),
    )

    sku = db.Column(db.String(50), nullable=False, unique=True)
    seller_id = db.Column(
        BigIntegerType, db.ForeignKey("users.id"), nullable=False, index=True
    )
    category_id = db.Column(
        BigIntegerType, db.ForeignKey("categories.id"), nullable=False, index=True
    )
    title = db.Column(db.String(200), nullable=False, index=True)
    slug = db.Column(db.String(255), nullable=False, unique=True)
    description = db.Column(db.Text, nullable=False)
    price = db.Column(db.Numeric(10, 2), nullable=False)
    sold_price = db.Column(db.Numeric(10, 2), nullable=True)
    condition = db.Column(
        db.Enum(ListingCondition, values_callable=_enum_values, name="listing_condition"),
        nullable=False,
        default=ListingCondition.GOOD,
        index=True,
    )
    status = db.Column(
        db.Enum(ListingStatus, values_callable=_enum_values, name="listing_status"),
        nullable=False,
        default=ListingStatus.DRAFT,
        index=True,
    )
    visibility = db.Column(
        db.Enum(ListingVisibility, values_callable=_enum_values, name="listing_visibility"),
        nullable=False,
        default=ListingVisibility.PUBLIC,
        index=True,
    )
    negotiable = db.Column(db.Boolean, nullable=False, default=True)
    is_featured = db.Column(db.Boolean, nullable=False, default=False, index=True)
    # When the listing became featured (used to order featured listings, most
    # recently featured first). Null when the listing is not featured.
    featured_at = db.Column(db.DateTime(timezone=True), nullable=True, index=True)
    # When the featured period ends. A listing is only *currently* featured while
    # this is in the future; past this it behaves as a normal listing.
    featured_until = db.Column(db.DateTime(timezone=True), nullable=True, index=True)
    location = db.Column(db.String(150), nullable=True)
    review_status = db.Column(
        db.Enum(ReviewStatus, values_callable=_enum_values, name="review_status"),
        nullable=False,
        default=ReviewStatus.APPROVED,
        index=True,
    )
    reviewed_by = db.Column(BigIntegerType, db.ForeignKey("users.id"), nullable=True)
    updated_by = db.Column(BigIntegerType, db.ForeignKey("users.id"), nullable=True)
    published_at = db.Column(db.DateTime, nullable=True)
    expires_at = db.Column(db.DateTime, nullable=True, index=True)
    deleted_at = db.Column(db.DateTime, nullable=True, index=True)
    view_count = db.Column(db.Integer, nullable=False, default=0)
    favorite_count = db.Column(db.Integer, nullable=False, default=0)
    offer_count = db.Column(db.Integer, nullable=False, default=0)

    # Relationships. seller uses an explicit foreign_keys because the table has
    # three FKs to users.id (seller_id, reviewed_by, updated_by); the latter two
    # remain plain columns for now.
    seller = db.relationship(
        "User", foreign_keys=[seller_id], backref="listings"
    )
    category = db.relationship("Category", backref="listings")
    images = db.relationship(
        "ListingImage",
        back_populates="listing",
        cascade="all, delete-orphan",
        order_by="ListingImage.display_order",
    )

    def __repr__(self) -> str:
        return f"<Listing id={self.id} sku={self.sku!r} title={self.title!r}>"


class ListingImage(BaseModel, PublicIdMixin, CreatedAtMixin):
    """An image attached to a listing (1–7 per listing)."""

    __tablename__ = "listing_images"

    listing_id = db.Column(
        BigIntegerType, db.ForeignKey("listings.id"), nullable=False, index=True
    )
    image_path = db.Column(db.String(255), nullable=False)
    caption = db.Column(db.String(100), nullable=True)
    display_order = db.Column(db.Integer, nullable=False, default=1, index=True)

    listing = db.relationship("Listing", back_populates="images")

    def __repr__(self) -> str:
        return f"<ListingImage id={self.id} listing_id={self.listing_id}>"


class Favorite(BaseModel, PublicIdMixin, CreatedAtMixin):
    """A student's saved listing. A user may favorite a listing only once."""

    __tablename__ = "favorites"
    __table_args__ = (
        db.UniqueConstraint("user_id", "listing_id", name="uq_favorites_user_listing"),
    )

    user_id = db.Column(
        BigIntegerType, db.ForeignKey("users.id"), nullable=False, index=True
    )
    listing_id = db.Column(
        BigIntegerType, db.ForeignKey("listings.id"), nullable=False, index=True
    )

    user = db.relationship(
        "User", backref=db.backref("favorites", cascade="all, delete-orphan")
    )
    listing = db.relationship(
        "Listing", backref=db.backref("favorites", cascade="all, delete-orphan")
    )

    def __repr__(self) -> str:
        return f"<Favorite id={self.id} user_id={self.user_id} listing_id={self.listing_id}>"


class ListingView(BaseModel):
    """A single (de-duplicated) view of a listing.

    An append-only event table backing view analytics. One row is written per
    genuine view (see ``record_listing_view``); the cached ``listings.view_count``
    stays the source displayed in the app, while these rows enable future
    historical analysis (daily views, unique/repeat visitors, trends) without any
    schema change.

    Privacy: raw IP addresses and user agents are **never** stored — only salted
    SHA-256 hashes (``ip_hash`` / ``user_agent_hash``).
    """

    __tablename__ = "listing_views"
    __table_args__ = (
        db.Index("ix_listing_views_listing_id", "listing_id"),
        db.Index("ix_listing_views_viewer_id", "viewer_id"),
        db.Index("ix_listing_views_viewed_at", "viewed_at"),
        db.Index("ix_listing_views_session_id", "session_id"),
        # Composite index supporting the anonymous duplicate-detection EXISTS.
        db.Index("ix_listing_views_dedup", "listing_id", "viewed_at"),
    )

    listing_id = db.Column(
        BigIntegerType, db.ForeignKey("listings.id"), nullable=False
    )
    # Null for anonymous visitors.
    viewer_id = db.Column(
        BigIntegerType, db.ForeignKey("users.id"), nullable=True
    )
    session_id = db.Column(db.String(64), nullable=True)
    ip_hash = db.Column(db.CHAR(64), nullable=True)
    user_agent_hash = db.Column(db.CHAR(64), nullable=True)
    viewed_at = db.Column(
        db.DateTime(timezone=True), nullable=False, default=utcnow
    )

    listing = db.relationship("Listing", backref=db.backref("views", cascade="all, delete-orphan"))
    viewer = db.relationship("User", foreign_keys=[viewer_id])

    def __repr__(self) -> str:
        return f"<ListingView id={self.id} listing_id={self.listing_id} viewer_id={self.viewer_id}>"
