from __future__ import annotations

import enum
from datetime import date, datetime
from decimal import Decimal

from sqlalchemy import (
    CheckConstraint,
    Date,
    DateTime,
    Enum,
    ForeignKey,
    Numeric,
    String,
    UniqueConstraint,
    func,
)
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship


class Section(str, enum.Enum):
    income = "income"
    fixed_bill = "fixed_bill"
    debt_minimum = "debt_minimum"
    food = "food"
    subscription = "subscription"
    other = "other"
    sinking_fund = "sinking_fund"


class MonthState(str, enum.Enum):
    planning = "planning"
    active = "active"
    closed = "closed"


SECTION_LABELS: dict[Section, str] = {
    Section.income: "Incomes",
    Section.fixed_bill: "Fixed Amount Bills",
    Section.debt_minimum: "Debt Minimums",
    Section.food: "Food and Essentials",
    Section.subscription: "Subscriptions",
    Section.other: "Other",
    Section.sinking_fund: "Sinking Funds",
}


class Base(DeclarativeBase):
    pass


class Entry(Base):
    __tablename__ = "entry"

    id: Mapped[int] = mapped_column(primary_key=True)
    section: Mapped[Section] = mapped_column(
        Enum(Section, native_enum=False, length=32), nullable=False, index=True
    )
    name: Mapped[str] = mapped_column(String(128), nullable=False)
    amount: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False)
    notes: Mapped[str | None] = mapped_column(String(1024), nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=func.now(), nullable=False
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        server_default=func.now(),
        onupdate=func.now(),
        nullable=False,
    )


class DebtTarget(Base):
    __tablename__ = "debt_target"
    __table_args__ = (CheckConstraint("id = 1", name="debt_target_singleton"),)

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
    debt_minimum_id: Mapped[int | None] = mapped_column(
        ForeignKey("entry.id", ondelete="SET NULL"), nullable=True
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        server_default=func.now(),
        onupdate=func.now(),
        nullable=False,
    )

    entry: Mapped[Entry | None] = relationship(Entry, lazy="joined")


class Month(Base):
    __tablename__ = "month"

    id: Mapped[int] = mapped_column(primary_key=True)
    year_month: Mapped[str] = mapped_column(String(7), nullable=False, unique=True)
    state: Mapped[MonthState] = mapped_column(
        Enum(MonthState, native_enum=False, length=16),
        nullable=False,
        default=MonthState.planning,
        server_default=MonthState.planning.value,
    )
    activated_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True), nullable=True
    )
    closed_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True), nullable=True
    )
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=func.now(), nullable=False
    )

    entries: Mapped[list["MonthEntry"]] = relationship(
        back_populates="month", cascade="all, delete-orphan"
    )
    target: Mapped["MonthDebtTarget | None"] = relationship(
        back_populates="month",
        cascade="all, delete-orphan",
        uselist=False,
        lazy="joined",
    )


class MonthEntry(Base):
    __tablename__ = "month_entry"
    __table_args__ = (UniqueConstraint("month_id", "id"),)

    id: Mapped[int] = mapped_column(primary_key=True)
    month_id: Mapped[int] = mapped_column(
        ForeignKey("month.id", ondelete="CASCADE"), nullable=False, index=True
    )
    section: Mapped[Section] = mapped_column(
        Enum(Section, native_enum=False, length=32), nullable=False, index=True
    )
    name: Mapped[str] = mapped_column(String(128), nullable=False)
    planned: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False)
    notes: Mapped[str | None] = mapped_column(String(1024), nullable=True)
    origin_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
    origin_planned: Mapped[Decimal | None] = mapped_column(
        Numeric(10, 2), nullable=True
    )
    source_entry_id: Mapped[int | None] = mapped_column(
        ForeignKey("entry.id", ondelete="SET NULL"), nullable=True
    )
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=func.now(), nullable=False
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        server_default=func.now(),
        onupdate=func.now(),
        nullable=False,
    )

    month: Mapped[Month] = relationship(back_populates="entries")
    postings: Mapped[list["Posting"]] = relationship(
        back_populates="entry",
        cascade="all, delete-orphan",
        order_by="Posting.occurred_on.desc(), Posting.id.desc()",
        lazy="selectin",
    )

    @property
    def applied(self) -> Decimal:
        return sum((p.amount for p in self.postings), Decimal("0")).quantize(
            Decimal("0.01")
        )


class MonthDebtTarget(Base):
    __tablename__ = "month_debt_target"

    month_id: Mapped[int] = mapped_column(
        ForeignKey("month.id", ondelete="CASCADE"),
        primary_key=True,
        autoincrement=False,
    )
    month_entry_id: Mapped[int | None] = mapped_column(
        ForeignKey("month_entry.id", ondelete="SET NULL"), nullable=True
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        server_default=func.now(),
        onupdate=func.now(),
        nullable=False,
    )

    month: Mapped[Month] = relationship(back_populates="target")
    entry: Mapped[MonthEntry | None] = relationship(MonthEntry, lazy="joined")


class Posting(Base):
    __tablename__ = "posting"

    id: Mapped[int] = mapped_column(primary_key=True)
    month_entry_id: Mapped[int] = mapped_column(
        ForeignKey("month_entry.id", ondelete="CASCADE"),
        nullable=False,
        index=True,
    )
    occurred_on: Mapped[date] = mapped_column(Date, nullable=False)
    amount: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False)
    description: Mapped[str | None] = mapped_column(String(256), nullable=True)
    payee: Mapped[str | None] = mapped_column(String(128), nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=func.now(), nullable=False
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        server_default=func.now(),
        onupdate=func.now(),
        nullable=False,
    )

    entry: Mapped[MonthEntry] = relationship(back_populates="postings")
