"""Design-direction options: CRUD, publish, reorder, and reviewer ordering.

Admin screens always see canonical sort_order. Clients see a per-person shuffle
that is stable across visits (locked decision D11): the seed is a hash of
username + project id, so resuming a half-done review keeps the same order while
no two reviewers share one — order bias becomes noise across the group instead
of a constant.
"""

import hashlib

from sqlalchemy import func, select
from sqlalchemy.orm import Session

from app.constants import OptionStatus
from app.models.option import Option
from app.services import storage


def list_for_admin(db: Session, project_id: int) -> list[Option]:
    stmt = (
        select(Option)
        .where(Option.project_id == project_id)
        .order_by(Option.sort_order, Option.id)
    )
    return list(db.scalars(stmt))


def list_published(db: Session, project_id: int) -> list[Option]:
    stmt = (
        select(Option)
        .where(Option.project_id == project_id, Option.status == OptionStatus.PUBLISHED)
        .order_by(Option.sort_order, Option.id)
    )
    return list(db.scalars(stmt))


def reviewer_order(options: list[Option], username: str, project_id: int) -> list[Option]:
    """The D11 shuffle: deterministic per (username, project), uniform-ish, and
    indifferent to when options were added."""

    def sort_key(option: Option) -> str:
        seed = f"{username}:{project_id}:{option.id}"
        return hashlib.sha256(seed.encode()).hexdigest()

    return sorted(options, key=sort_key)


def get_option(db: Session, project_id: int, option_id: int) -> Option | None:
    option = db.get(Option, option_id)
    if option is None or option.project_id != project_id:
        return None
    return option


def next_sort_order(db: Session, project_id: int) -> int:
    current = db.scalar(
        select(func.coalesce(func.max(Option.sort_order), 0)).where(
            Option.project_id == project_id
        )
    )
    return (current or 0) + 1


def next_display_label(db: Session, project_id: int) -> str:
    """Option A, Option B, … continuing past Z as AA, AB in the unlikely case."""
    count = db.scalar(select(func.count()).select_from(Option).where(Option.project_id == project_id)) or 0
    label, n = "", count
    while True:
        label = chr(ord("A") + n % 26) + label
        n = n // 26 - 1
        if n < 0:
            break
    return f"Option {label}"


def update(db: Session, option: Option, **fields) -> Option:
    for key, value in fields.items():
        if value is not None:
            setattr(option, key, value.strip() if isinstance(value, str) else value)
    db.commit()
    db.refresh(option)
    return option


def reorder(db: Session, project_id: int, ordered_ids: list[int]) -> list[Option]:
    """Assign sort_order from the given id order. Ids not listed keep their
    relative order after the listed ones — a partial reorder cannot lose rows."""
    options = list_for_admin(db, project_id)
    by_id = {option.id: option for option in options}
    position = 1
    for option_id in ordered_ids:
        option = by_id.pop(option_id, None)
        if option is not None:
            option.sort_order = position
            position += 1
    for option in by_id.values():  # anything the caller did not mention
        option.sort_order = position
        position += 1
    db.commit()
    return list_for_admin(db, project_id)


def delete(db: Session, option: Option) -> None:
    """Remove the option and its screenshots. The router refuses when reviews
    exist; by the time we are here the cascade has nothing meaningful to eat."""
    desktop, mobile = option.screenshot_desktop, option.screenshot_mobile
    db.delete(option)
    db.commit()
    storage.delete_file(desktop)
    storage.delete_file(mobile)
