import datetime
import uuid
from typing import TYPE_CHECKING

from sqlalchemy import Boolean
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy import Text
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from sqlalchemy_json import MutableJson

import canaille.oidc.models

from ..backend import Base
from ..utils import TZDateTime
from .core import SqlAlchemyModel

if TYPE_CHECKING:
    from .core import User


class ClientAudience(Base):
    """Association object for Client.audience with ordering support."""

    __tablename__ = "client_audience_association_table"

    client_id: Mapped[str] = mapped_column(
        ForeignKey("client.id", ondelete="CASCADE"), primary_key=True, nullable=True
    )
    audience_id: Mapped[str] = mapped_column(
        ForeignKey("client.id", ondelete="CASCADE"), primary_key=True, nullable=True
    )
    index: Mapped[int] = mapped_column(Integer)

    audience: Mapped["Client"] = relationship(
        "Client", foreign_keys=[audience_id], lazy="joined"
    )


class Client(canaille.oidc.models.Client, Base, SqlAlchemyModel):
    __tablename__ = "client"

    id: Mapped[str] = mapped_column(
        String(36), primary_key=True, default=lambda: str(uuid.uuid4())
    )
    created: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )
    last_modified: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )

    description: Mapped[str] = mapped_column(Text, nullable=True)
    post_logout_redirect_uris: Mapped[list[str]] = mapped_column(
        MutableJson, nullable=True
    )
    _audience_association: Mapped[list["ClientAudience"]] = relationship(
        "ClientAudience",
        foreign_keys="ClientAudience.client_id",
        order_by="ClientAudience.index",
        collection_class=ordering_list("index"),
        cascade="all, delete-orphan",
    )
    audience = association_proxy(
        "_audience_association",
        "audience",
        creator=lambda aud: ClientAudience(audience=aud),
    )
    client_id: Mapped[str] = mapped_column(String(255), nullable=True)
    client_secret: Mapped[str] = mapped_column(String(255), nullable=True)
    client_id_issued_at: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )
    client_secret_expires_at: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )
    client_name: Mapped[str] = mapped_column(String(255), nullable=True)
    contacts: Mapped[list[str]] = mapped_column(MutableJson, nullable=True)
    client_uri: Mapped[str] = mapped_column(String(2048), nullable=True)
    redirect_uris: Mapped[list[str]] = mapped_column(MutableJson, nullable=True)
    logo_uri: Mapped[str] = mapped_column(String(2048), nullable=True)
    grant_types: Mapped[list[str]] = mapped_column(MutableJson, nullable=True)
    response_types: Mapped[list[str]] = mapped_column(MutableJson, nullable=True)
    scope: Mapped[list[str]] = mapped_column(MutableJson, nullable=True)
    tos_uri: Mapped[str] = mapped_column(String(2048), nullable=True)
    policy_uri: Mapped[str] = mapped_column(String(2048), nullable=True)
    jwks_uri: Mapped[str] = mapped_column(String(2048), nullable=True)
    jwks: Mapped[str] = mapped_column(Text, nullable=True)
    token_endpoint_auth_method: Mapped[str] = mapped_column(String(50), nullable=True)
    token_endpoint_auth_signing_alg: Mapped[str] = mapped_column(
        String(50), nullable=True
    )
    sector_identifier_uri: Mapped[str] = mapped_column(String(2048), nullable=True)
    subject_type: Mapped[str] = mapped_column(String(20), nullable=True)
    software_id: Mapped[str] = mapped_column(String(255), nullable=True)
    software_version: Mapped[str] = mapped_column(String(100), nullable=True)
    application_type: Mapped[str] = mapped_column(String(20), nullable=True)
    id_token_signed_response_alg: Mapped[str] = mapped_column(String(50), nullable=True)
    id_token_encrypted_response_alg: Mapped[str] = mapped_column(
        String(50), nullable=True
    )
    id_token_encrypted_response_enc: Mapped[str] = mapped_column(
        String(50), nullable=True
    )
    userinfo_signed_response_alg: Mapped[str] = mapped_column(String(50), nullable=True)
    userinfo_encrypted_response_alg: Mapped[str] = mapped_column(
        String(50), nullable=True
    )
    userinfo_encrypted_response_enc: Mapped[str] = mapped_column(
        String(50), nullable=True
    )
    default_max_age: Mapped[int] = mapped_column(Integer, nullable=True)
    require_auth_time: Mapped[bool] = mapped_column(Boolean, nullable=True)
    default_acr_values: Mapped[list[str]] = mapped_column(
        MutableJson, nullable=True, default=[]
    )
    initiate_login_uri: Mapped[str] = mapped_column(String(2048), nullable=True)
    request_object_signing_alg: Mapped[str] = mapped_column(String(50), nullable=True)
    request_object_encryption_alg: Mapped[str] = mapped_column(
        String(50), nullable=True
    )
    request_object_encryption_enc: Mapped[str] = mapped_column(
        String(50), nullable=True
    )
    request_uris: Mapped[list[str]] = mapped_column(
        MutableJson, nullable=True, default=[]
    )
    require_signed_request_object: Mapped[bool] = mapped_column(Boolean, nullable=True)


class AuthorizationCode(canaille.oidc.models.AuthorizationCode, Base, SqlAlchemyModel):
    __tablename__ = "authorization_code"

    id: Mapped[str] = mapped_column(
        String(36), primary_key=True, default=lambda: str(uuid.uuid4())
    )
    created: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )
    last_modified: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )

    authorization_code_id: Mapped[str] = mapped_column(String(255), nullable=True)
    code: Mapped[str] = mapped_column(String(255), nullable=True)
    client_id: Mapped[str] = mapped_column(ForeignKey("client.id", ondelete="CASCADE"))
    client: Mapped["Client"] = relationship()
    subject_id: Mapped[str] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"))
    subject: Mapped["User"] = relationship()
    redirect_uri: Mapped[str] = mapped_column(String(2048), nullable=True)
    response_type: Mapped[str] = mapped_column(String(100), nullable=True)
    scope: Mapped[list[str]] = mapped_column(MutableJson, nullable=True)
    nonce: Mapped[str] = mapped_column(String(255), nullable=True)
    issue_date: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )
    lifetime: Mapped[int] = mapped_column(Integer, nullable=True)
    challenge: Mapped[str] = mapped_column(String(255), nullable=True)
    challenge_method: Mapped[str] = mapped_column(String(20), nullable=True)
    revokation_date: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )
    auth_time: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )
    acr: Mapped[str] = mapped_column(String(255), nullable=True)
    amr: Mapped[list[str]] = mapped_column(MutableJson, nullable=True)


class TokenAudience(Base):
    """Association object for Token.audience with ordering support."""

    __tablename__ = "token_audience_association_table"

    token_id: Mapped[str] = mapped_column(
        ForeignKey("token.id", ondelete="CASCADE"), primary_key=True, nullable=True
    )
    client_id: Mapped[str] = mapped_column(
        ForeignKey("client.id", ondelete="CASCADE"), primary_key=True, nullable=True
    )
    index: Mapped[int] = mapped_column(Integer)

    audience: Mapped["Client"] = relationship(
        "Client", foreign_keys=[client_id], lazy="joined"
    )


class Token(canaille.oidc.models.Token, Base, SqlAlchemyModel):
    __tablename__ = "token"

    id: Mapped[str] = mapped_column(
        String(36), primary_key=True, default=lambda: str(uuid.uuid4())
    )
    created: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )
    last_modified: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )

    token_id: Mapped[str] = mapped_column(String(255), nullable=True)
    access_token: Mapped[str] = mapped_column(Text, nullable=True, unique=True)
    client_id: Mapped[str] = mapped_column(ForeignKey("client.id", ondelete="CASCADE"))
    client: Mapped["Client"] = relationship()
    subject_id: Mapped[str] = mapped_column(
        ForeignKey("user.id", ondelete="CASCADE"), nullable=True
    )
    subject: Mapped["User"] = relationship()
    type: Mapped[str] = mapped_column(String(50), nullable=True)
    refresh_token: Mapped[str] = mapped_column(Text, nullable=True, unique=True)
    scope: Mapped[list[str]] = mapped_column(MutableJson, nullable=True)
    issue_date: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )
    lifetime: Mapped[int] = mapped_column(Integer, nullable=True)
    revokation_date: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )
    _audience_association: Mapped[list["TokenAudience"]] = relationship(
        "TokenAudience",
        foreign_keys="TokenAudience.token_id",
        order_by="TokenAudience.index",
        collection_class=ordering_list("index"),
        cascade="all, delete-orphan",
    )
    audience = association_proxy(
        "_audience_association",
        "audience",
        creator=lambda aud: TokenAudience(audience=aud),
    )


class Consent(canaille.oidc.models.Consent, Base, SqlAlchemyModel):
    __tablename__ = "consent"

    id: Mapped[str] = mapped_column(
        String(36), primary_key=True, default=lambda: str(uuid.uuid4())
    )
    created: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )
    last_modified: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )

    consent_id: Mapped[str] = mapped_column(String(255), nullable=True)
    subject_id: Mapped[str] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"))
    subject: Mapped["User"] = relationship()
    client_id: Mapped[str] = mapped_column(ForeignKey("client.id", ondelete="CASCADE"))
    client: Mapped["Client"] = relationship()
    scope: Mapped[list[str]] = mapped_column(MutableJson, nullable=True)
    issue_date: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )
    revokation_date: Mapped[datetime.datetime] = mapped_column(
        TZDateTime(timezone=True), nullable=True
    )
