from __future__ import annotations

from datetime import datetime, timezone

from sqlalchemy import (JSON, BigInteger, Boolean, DateTime, Enum, ForeignKey, Index, Integer, String, Text,
                        UniqueConstraint)
from sqlalchemy.orm import Mapped, mapped_column, relationship

from ..database import Base


def utcnow() -> datetime:
    return datetime.now(timezone.utc).replace(tzinfo=None)


class TimestampMixin:
    created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, nullable=False)
    updated_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, onupdate=utcnow, nullable=False)


class Administrator(Base, TimestampMixin):
    __tablename__ = "administrators"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    username: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
    full_name: Mapped[str] = mapped_column(String(120), nullable=False)
    email: Mapped[str | None] = mapped_column(String(160))
    password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
    is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
    last_login_at: Mapped[datetime | None] = mapped_column(DateTime)


class Teacher(Base, TimestampMixin):
    __tablename__ = "teachers"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    username: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
    full_name: Mapped[str] = mapped_column(String(120), nullable=False)
    nip: Mapped[str | None] = mapped_column(String(40))
    email: Mapped[str | None] = mapped_column(String(160))
    password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
    # izin per guru: power, lock, message, screen, internet, browser, downloads, apps
    permissions: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
    is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
    last_login_at: Mapped[datetime | None] = mapped_column(DateTime)
    classrooms: Mapped[list["Classroom"]] = relationship(back_populates="teacher")


class Classroom(Base, TimestampMixin):
    __tablename__ = "classrooms"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(80), unique=True, nullable=False)
    description: Mapped[str | None] = mapped_column(String(255))
    teacher_id: Mapped[int | None] = mapped_column(ForeignKey("teachers.id", ondelete="SET NULL"))
    teacher: Mapped[Teacher | None] = relationship(back_populates="classrooms")


class Student(Base, TimestampMixin):
    __tablename__ = "students"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    nis: Mapped[str | None] = mapped_column(String(40), unique=True)
    full_name: Mapped[str] = mapped_column(String(120), nullable=False)
    classroom_id: Mapped[int | None] = mapped_column(ForeignKey("classrooms.id", ondelete="SET NULL"), index=True)
    computer_id: Mapped[int | None] = mapped_column(ForeignKey("computers.id", ondelete="SET NULL"), index=True)
    is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)


class Computer(Base, TimestampMixin):
    __tablename__ = "computers"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    device_uid: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
    hostname: Mapped[str] = mapped_column(String(120), nullable=False)
    label: Mapped[str | None] = mapped_column(String(120))
    ip_address: Mapped[str | None] = mapped_column(String(45))
    mac_address: Mapped[str | None] = mapped_column(String(17))
    windows_user: Mapped[str | None] = mapped_column(String(120))
    os_name: Mapped[str | None] = mapped_column(String(160))
    agent_version: Mapped[str | None] = mapped_column(String(20))
    classroom_id: Mapped[int | None] = mapped_column(ForeignKey("classrooms.id", ondelete="SET NULL"), index=True)
    is_online: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
    usage_state: Mapped[str] = mapped_column(String(20), default="offline", nullable=False)  # idle|in_use|locked|offline
    is_locked: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    internet_override: Mapped[str | None] = mapped_column(String(20))  # block_all | allow_all | NULL
    exam_lock: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    metrics: Mapped[dict | None] = mapped_column(JSON)
    last_seen_at: Mapped[datetime | None] = mapped_column(DateTime)
    policy_hash: Mapped[str | None] = mapped_column(String(64))
    policy_applied_at: Mapped[datetime | None] = mapped_column(DateTime)
    released: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    classroom: Mapped[Classroom | None] = relationship()
    student: Mapped[Student | None] = relationship(primaryjoin="Computer.id==foreign(Student.computer_id)", uselist=False,
                                                   viewonly=True)


class DeviceToken(Base, TimestampMixin):
    __tablename__ = "device_tokens"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    computer_id: Mapped[int] = mapped_column(ForeignKey("computers.id", ondelete="CASCADE"), nullable=False, index=True)
    token_hash: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
    expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
    revoked: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)


class RefreshToken(Base):
    __tablename__ = "refresh_tokens"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    token_hash: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
    role: Mapped[str] = mapped_column(String(10), nullable=False)
    user_id: Mapped[int] = mapped_column(Integer, nullable=False)
    expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
    revoked: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, nullable=False)


class ComputerSession(Base):
    """Riwayat login pengguna/penggunaan komputer (untuk 'waktu penggunaan') dan sesi pemantauan layar."""
    __tablename__ = "computer_sessions"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    computer_id: Mapped[int] = mapped_column(ForeignKey("computers.id", ondelete="CASCADE"), nullable=False, index=True)
    kind: Mapped[str] = mapped_column(String(20), nullable=False)  # usage | screen_view | remote_control
    windows_user: Mapped[str | None] = mapped_column(String(120))
    started_by: Mapped[str | None] = mapped_column(String(120))
    started_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, nullable=False)
    ended_at: Mapped[datetime | None] = mapped_column(DateTime)


class ComputerCommand(Base):
    __tablename__ = "computer_commands"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    computer_id: Mapped[int] = mapped_column(ForeignKey("computers.id", ondelete="CASCADE"), nullable=False, index=True)
    action: Mapped[str] = mapped_column(String(40), nullable=False)
    params: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
    status: Mapped[str] = mapped_column(String(12), default="pending", nullable=False, index=True)  # pending|sent|success|failed|expired
    result: Mapped[str | None] = mapped_column(Text)
    reason: Mapped[str | None] = mapped_column(String(255))
    issued_by_role: Mapped[str] = mapped_column(String(10), nullable=False)
    issued_by_id: Mapped[int | None] = mapped_column(Integer)
    issued_by_name: Mapped[str] = mapped_column(String(120), nullable=False)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, nullable=False, index=True)
    sent_at: Mapped[datetime | None] = mapped_column(DateTime)
    completed_at: Mapped[datetime | None] = mapped_column(DateTime)
    expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)


class InternetPolicy(Base, TimestampMixin):
    __tablename__ = "internet_policies"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
    mode: Mapped[str] = mapped_column(String(12), default="blocklist", nullable=False)  # allow_all|block_all|allowlist|blocklist
    block_social: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    block_games: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    block_entertainment: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
    created_by: Mapped[str | None] = mapped_column(String(120))
    blocked: Mapped[list["BlockedWebsite"]] = relationship(cascade="all, delete-orphan", back_populates="policy")
    allowed: Mapped[list["AllowedWebsite"]] = relationship(cascade="all, delete-orphan", back_populates="policy")


class BlockedWebsite(Base):
    __tablename__ = "blocked_websites"
    __table_args__ = (UniqueConstraint("policy_id", "domain", name="uq_blocked_policy_domain"),)
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    policy_id: Mapped[int] = mapped_column(ForeignKey("internet_policies.id", ondelete="CASCADE"), nullable=False, index=True)
    domain: Mapped[str] = mapped_column(String(190), nullable=False)
    category: Mapped[str | None] = mapped_column(String(30))
    created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, nullable=False)
    policy: Mapped[InternetPolicy] = relationship(back_populates="blocked")


class AllowedWebsite(Base):
    __tablename__ = "allowed_websites"
    __table_args__ = (UniqueConstraint("policy_id", "domain", name="uq_allowed_policy_domain"),)
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    policy_id: Mapped[int] = mapped_column(ForeignKey("internet_policies.id", ondelete="CASCADE"), nullable=False, index=True)
    domain: Mapped[str] = mapped_column(String(190), nullable=False)
    category: Mapped[str | None] = mapped_column(String(30))
    created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, nullable=False)
    policy: Mapped[InternetPolicy] = relationship(back_populates="allowed")


class BrowserPolicy(Base, TimestampMixin):
    __tablename__ = "browser_policies"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
    max_tabs: Mapped[int] = mapped_column(Integer, default=0, nullable=False)  # 0 = tanpa batas
    allowed_browsers: Mapped[list] = mapped_column(JSON, default=lambda: ["chrome", "edge"], nullable=False)
    homepage: Mapped[str | None] = mapped_column(String(255))
    block_incognito: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
    downloads_require_approval: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
    exam_mode: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)  # hanya whitelist, tab dibatasi
    is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)


class ApplicationPolicy(Base, TimestampMixin):
    __tablename__ = "application_policies"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
    mode: Mapped[str] = mapped_column(String(10), default="blocklist", nullable=False)  # blocklist | allowlist
    blocked_apps: Mapped[list] = mapped_column(JSON, default=list, nullable=False)  # nama exe
    allowed_apps: Mapped[list] = mapped_column(JSON, default=list, nullable=False)
    block_store: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
    block_installers: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
    is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)


class PolicyAssignment(Base):
    __tablename__ = "policy_assignments"
    __table_args__ = (UniqueConstraint("policy_type", "target_type", "target_id", name="uq_assign"),
                      Index("ix_assign_target", "target_type", "target_id"))
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    policy_type: Mapped[str] = mapped_column(String(12), nullable=False)  # internet|browser|application
    policy_id: Mapped[int] = mapped_column(Integer, nullable=False)
    target_type: Mapped[str] = mapped_column(String(12), nullable=False)  # all|classroom|computer
    target_id: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, nullable=False)


class Schedule(Base, TimestampMixin):
    """Jadwal pembelajaran: dalam jendela waktu, terapkan kebijakan tertentu atau izinkan penggunaan komputer."""
    __tablename__ = "schedules"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(100), nullable=False)
    kind: Mapped[str] = mapped_column(String(14), nullable=False)  # internet|browser|application|usage
    classroom_id: Mapped[int | None] = mapped_column(ForeignKey("classrooms.id", ondelete="CASCADE"), index=True)
    computer_id: Mapped[int | None] = mapped_column(ForeignKey("computers.id", ondelete="CASCADE"), index=True)
    days: Mapped[list] = mapped_column(JSON, default=lambda: [0, 1, 2, 3, 4], nullable=False)  # 0=Senin
    start_time: Mapped[str] = mapped_column(String(5), nullable=False)  # HH:MM
    end_time: Mapped[str] = mapped_column(String(5), nullable=False)
    policy_id: Mapped[int | None] = mapped_column(Integer)  # untuk kind != usage
    is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)


class DownloadRequest(Base):
    __tablename__ = "download_requests"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    computer_id: Mapped[int] = mapped_column(ForeignKey("computers.id", ondelete="CASCADE"), nullable=False, index=True)
    student_id: Mapped[int | None] = mapped_column(ForeignKey("students.id", ondelete="SET NULL"))
    student_name: Mapped[str | None] = mapped_column(String(120))
    file_name: Mapped[str] = mapped_column(String(255), nullable=False)
    file_size: Mapped[int | None] = mapped_column(BigInteger)
    file_type: Mapped[str | None] = mapped_column(String(120))
    source_url: Mapped[str] = mapped_column(Text, nullable=False)
    browser: Mapped[str | None] = mapped_column(String(20))
    status: Mapped[str] = mapped_column(Enum("menunggu", "disetujui", "ditolak", "kedaluwarsa", name="dl_status"),
                                        default="menunggu", nullable=False, index=True)
    auto_decision: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    decision_note: Mapped[str | None] = mapped_column(String(255))
    decided_by: Mapped[str | None] = mapped_column(String(120))
    decided_at: Mapped[datetime | None] = mapped_column(DateTime)
    requested_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, nullable=False, index=True)
    expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
    computer: Mapped[Computer] = relationship()


class DownloadApproval(Base):
    __tablename__ = "download_approvals"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    request_id: Mapped[int] = mapped_column(ForeignKey("download_requests.id", ondelete="CASCADE"), unique=True, nullable=False)
    approved_by: Mapped[str] = mapped_column(String(120), nullable=False)
    approved_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, nullable=False)
    valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=False)
    single_use: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
    used_at: Mapped[datetime | None] = mapped_column(DateTime)


class ApplicationRequest(Base):
    __tablename__ = "application_requests"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    computer_id: Mapped[int] = mapped_column(ForeignKey("computers.id", ondelete="CASCADE"), nullable=False, index=True)
    student_name: Mapped[str | None] = mapped_column(String(120))
    app_name: Mapped[str] = mapped_column(String(255), nullable=False)
    request_type: Mapped[str] = mapped_column(String(12), default="install", nullable=False)  # install | run | store
    detail: Mapped[str | None] = mapped_column(String(500))
    status: Mapped[str] = mapped_column(Enum("menunggu", "disetujui", "ditolak", "kedaluwarsa", name="app_status"),
                                        default="menunggu", nullable=False, index=True)
    permit_minutes: Mapped[int | None] = mapped_column(Integer)
    decision_note: Mapped[str | None] = mapped_column(String(255))
    decided_by: Mapped[str | None] = mapped_column(String(120))
    decided_at: Mapped[datetime | None] = mapped_column(DateTime)
    requested_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, nullable=False, index=True)
    computer: Mapped[Computer] = relationship()


class InstalledApplication(Base):
    __tablename__ = "installed_applications"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    computer_id: Mapped[int] = mapped_column(ForeignKey("computers.id", ondelete="CASCADE"), nullable=False, index=True)
    name: Mapped[str] = mapped_column(String(255), nullable=False)
    version: Mapped[str | None] = mapped_column(String(80))
    publisher: Mapped[str | None] = mapped_column(String(160))
    install_date: Mapped[str | None] = mapped_column(String(20))
    first_seen_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, nullable=False)
    reported_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, nullable=False)


class Notification(Base):
    __tablename__ = "notifications"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    type: Mapped[str] = mapped_column(String(30), nullable=False, index=True)
    title: Mapped[str] = mapped_column(String(160), nullable=False)
    message: Mapped[str | None] = mapped_column(String(500))
    severity: Mapped[str] = mapped_column(String(10), default="info", nullable=False)
    computer_id: Mapped[int | None] = mapped_column(ForeignKey("computers.id", ondelete="CASCADE"), index=True)
    classroom_id: Mapped[int | None] = mapped_column(Integer, index=True)
    is_read: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, nullable=False, index=True)


class ActivityLog(Base):
    """Audit log tamper-evident: setiap baris menyimpan hash baris sebelumnya."""
    __tablename__ = "activity_logs"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    actor_type: Mapped[str] = mapped_column(String(10), nullable=False)  # admin|guru|agent|system|siswa
    actor_id: Mapped[int | None] = mapped_column(Integer)
    actor_name: Mapped[str] = mapped_column(String(120), nullable=False)
    action: Mapped[str] = mapped_column(String(60), nullable=False, index=True)
    computer_id: Mapped[int | None] = mapped_column(Integer, index=True)
    computer_name: Mapped[str | None] = mapped_column(String(120))
    status: Mapped[str] = mapped_column(String(10), default="success", nullable=False)
    detail: Mapped[dict | None] = mapped_column(JSON)
    reason: Mapped[str | None] = mapped_column(String(255))
    ip_address: Mapped[str | None] = mapped_column(String(45))
    created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, nullable=False, index=True)
    prev_hash: Mapped[str | None] = mapped_column(String(64))
    row_hash: Mapped[str | None] = mapped_column(String(64))


class SystemSetting(Base):
    __tablename__ = "system_settings"
    key: Mapped[str] = mapped_column(String(80), primary_key=True)
    value: Mapped[str | None] = mapped_column(Text)
    updated_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, onupdate=utcnow, nullable=False)
