"""Upload -> pending staging (imports.md §B, rebuild edition).

The two-step flow (02 §5 invariant 4): the CSV is parsed ONCE at
upload, the normalized entries are staged as jsonb, and nothing touches
the real tables until the human has resolved every unknown name. All
attribution uses the EFFECTIVE user (ADR #033 carry-over — view-as is
true impersonation).

Expiry is enforced (T-IMP-031, fixing legacy wart #5): expired rows are
purged on every upload and read as not-found (then deleted) on access.
"""

from __future__ import annotations

import re
import uuid
from datetime import UTC, datetime, timedelta

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

from app.models import PendingImport, User
from app.services.imports.clockify import (
    ParseResult,
    format_date_range_label,
    parse_clockify_csv,
)
from app.services.imports.errors import ImportsError

MAX_UPLOAD_BYTES = 25 * 1024 * 1024  # imports.md B8

PENDING_NOT_FOUND = ImportsError(
    404,
    "PENDING_NOT_FOUND",
    "Pending import not found. It may have expired (they last 24h) or"
    " someone else cancelled it.",
)


def _now() -> datetime:
    return datetime.now(UTC)


def _as_aware(value: datetime) -> datetime:
    """SQLite returns naive datetimes for timezone=True columns —
    normalize to UTC-aware before comparing."""
    return value if value.tzinfo is not None else value.replace(tzinfo=UTC)


def require_can_manage_imports(actor, *, verb: str = "import") -> None:
    if not actor.allowed("can_manage_imports"):
        raise ImportsError(403, "NOT_ALLOWED", f"You do not have permission to {verb}.")


def _require_account(actor) -> None:
    """Fail-fast BEFORE staging (T-IMP-007's analogue): uploads need a
    tenant context and a users row to attribute uploaded_by to."""
    if actor.tenant_id is None or actor.user_id is None:
        raise ImportsError(
            400,
            "NO_ACCOUNT",
            "You don't have an account in this organization yet."
            " An owner needs to set up your access before you can import.",
        )


def purge_expired(session: Session, tenant_id: uuid.UUID) -> int:
    result = session.execute(
        delete(PendingImport).where(
            PendingImport.tenant_id == tenant_id,
            PendingImport.expires_at < _now(),
        ),
        # don't Python-evaluate the criteria against in-session objects —
        # SQLite hands back naive datetimes and the comparison would blow up
        execution_options={"synchronize_session": False},
    )
    return result.rowcount or 0


def batch_name_for(
    typed_name: str | None, parse: ParseResult, filename: str | None
) -> str | None:
    """imports.md B10: typed (trimmed, non-blank) name wins; else the
    CSV date-range label; else the filename sans its last extension."""
    typed = (typed_name or "").strip()
    if typed:
        return typed
    label = format_date_range_label(parse.date_range)
    if label:
        return label
    if filename:
        return re.sub(r"\.[^.]+$", "", filename)
    return None


def stage_upload(
    session: Session,
    actor,
    *,
    filename: str | None,
    data: bytes,
    batch_name: str | None = None,
    notes: str | None = None,
) -> PendingImport:
    """Validate -> parse -> stage. Raises ImportsError with the pinned
    legacy messages (B5, B8, B9); commits the staging row."""
    require_can_manage_imports(actor)
    _require_account(actor)

    if not data:
        raise ImportsError(400, "NO_FILE", "Please choose a CSV file to upload.")
    if len(data) > MAX_UPLOAD_BYTES:
        raise ImportsError(400, "FILE_TOO_LARGE", "File too large (25MB max).")

    csv_text = data.decode("utf-8-sig", errors="replace")
    parse = parse_clockify_csv(csv_text)
    if not parse.entries:
        message = (
            parse.errors[0]["message"] if parse.errors else "No valid rows found in the CSV."
        )
        raise ImportsError(400, "PARSE_FAILED", message)

    purge_expired(session, actor.tenant_id)

    pending = PendingImport(
        tenant_id=actor.tenant_id,
        uploaded_by=actor.user_id,  # EFFECTIVE user (ADR #033)
        filename=filename,
        batch_name=batch_name_for(batch_name, parse, filename),
        notes=(notes or "").strip() or None,
        source="clockify",
        date_format=parse.date_format_used,
        parsed_entries=[e.as_dict() for e in parse.entries],
        parse_errors=parse.errors,
        parse_warnings=parse.warnings,
        total_rows=parse.total_rows,
        expires_at=_now() + timedelta(hours=24),
    )
    session.add(pending)
    session.commit()
    return pending


def get_pending(session: Session, actor, pending_id: uuid.UUID) -> PendingImport:
    """Visibility = the legacy RLS rule (imports.md B38/B40): uploader
    sees their own; owner / super-admin (not viewing-as) see all;
    everyone else gets not-found (never a 403 — existence isn't leaked).
    Expired rows are deleted and read as not-found (T-IMP-031)."""
    pending = session.get(PendingImport, pending_id)
    if pending is None or pending.tenant_id != actor.tenant_id:
        raise PENDING_NOT_FOUND
    if _as_aware(pending.expires_at) < _now():
        session.delete(pending)
        session.commit()
        raise PENDING_NOT_FOUND

    is_super = actor.is_super_admin and not actor.is_viewing_as
    if not (is_super or actor.role == "owner" or pending.uploaded_by == actor.user_id):
        raise PENDING_NOT_FOUND
    return pending


def can_commit(actor, pending: PendingImport) -> bool:
    """imports.md B46: can_manage_imports AND (own pending OR
    can_transfer) — managers commit their own, owners/super-admins
    commit anyone's."""
    return actor.allowed("can_manage_imports") and (
        pending.uploaded_by == actor.user_id or actor.allowed("can_transfer")
    )


def cancel_pending(session: Session, actor, pending_id: uuid.UUID) -> None:
    """B70 rebuild: same visibility rule as reads (the legacy version
    leaned on RLS alone); deletes the staging row."""
    pending = get_pending(session, actor, pending_id)
    session.delete(pending)
    session.commit()


def uploader_username(session: Session, pending: PendingImport) -> str | None:
    return session.scalar(select(User.idauth_username).where(User.id == pending.uploaded_by))
