"""Input normalisation, sanitisation and registration constraint checks."""
import re

from nexora.utils.team import parse_team_members


# ─────────────────────────────────────────────────────────────────────────────
# Registration validation helpers
# ─────────────────────────────────────────────────────────────────────────────
def normalize_email(email):
    """Trim whitespace and lowercase for case-insensitive comparison."""
    return (email or "").strip().lower()


def normalize_cnic(cnic):
    """Strip spaces, hyphens, dots from CNIC so formatting cannot bypass checks."""
    return re.sub(r"[\s\-\.]", "", (cnic or "").strip())


def sanitize_field(value, max_len=None):
    """Strip control characters and enforce max length."""
    v = (value or "").strip()
    # Remove control chars except newline/tab
    v = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", v)
    if max_len and len(v) > max_len:
        v = v[:max_len]
    return v


def validate_email_format(email):
    """Basic email format check."""
    email = (email or "").strip()
    if not email or len(email) > 254:
        return False
    return bool(re.match(r"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$", email))


def validate_cnic_format(cnic):
    """CNIC should be 5-20 digits after normalization."""
    c = normalize_cnic(cnic)
    return c and c.isdigit() and 5 <= len(c) <= 20


def validate_phone_format(phone):
    """Phone should be 7-20 digits/dashes/plus."""
    p = re.sub(r"[\s\-\+]", "", (phone or "").strip())
    return p and p.isdigit() and 7 <= len(p) <= 20


def validate_username_format(username):
    """Username: 3-60 chars, alphanumeric plus underscore/hyphen/dot."""
    u = (username or "").strip()
    return bool(u) and 3 <= len(u) <= 60 and bool(re.match(r"^[a-zA-Z0-9._\-]+$", u))


# Only these table names may be used in f-string SQL (prevents SQL injection via table names)
_ALLOWED_TABLES = frozenset({"registrations", "member_tickets"})


def safe_table_name(name):
    """Return the table name only if it's in the allowlist, else raise ValueError."""
    if name not in _ALLOWED_TABLES:
        raise ValueError(f"Invalid table name: {name}")
    return name


def _emails_in_members(members_text):
    """Return the set of normalised emails found in a members text block."""
    return {
        normalize_email(m["email"])
        for m in parse_team_members(members_text)
        if m.get("email")
    }


def _q_overlap(selected_str, q_set):
    """True if any qualifier id in `selected_str` is also in `q_set`."""
    reg_q = {int(x) for x in (selected_str or "").split(",") if x.strip().isdigit()}
    return bool(reg_q & q_set)


def validate_registration_constraints(
    conn, email, cnic, game_id, team_name, entry_type,
    members_text=None, exclude_id=None, selected_q_ids=None,
):
    """Check every uniqueness rule inside *conn* (caller owns the transaction).

    Returns a list of user-facing error strings.  An empty list means valid.

    Rules (all game-scoped):
    1. Nickname / team name must be unique within the same game.
    2. CNIC uniqueness depends on qualifier mode:
        - No qualifiers: one player (CNIC) can only register ONCE per game.
        - With qualifiers: one player (CNIC) can register for multiple qualifiers
          but NOT the same qualifier twice.
    3. A player (email or CNIC) cannot already be registered for the same game
        (as primary registrant or as a team member) for the same qualifiers.
    """
    errors = []

    try:
        norm_email = normalize_email(email)
        norm_cnic = normalize_cnic(cnic)
        norm_team = (team_name or "").strip()
        game_id = int(game_id)
    except (ValueError, TypeError):
        errors.append("Invalid registration data. Please check your inputs.")
        return errors

    exclude = " AND id != ?" if exclude_id else ""
    exclude_args = (exclude_id,) if exclude_id else ()
    has_qualifiers = bool(selected_q_ids)
    q_id_set = set(int(x) for x in (selected_q_ids or []))

    # --- 1. Nickname / team name unique within the same game ---
    if norm_team:
        try:
            hit = conn.execute(
                f"""SELECT id FROM registrations
                    WHERE LOWER(TRIM(team_name)) = LOWER(?)
                      AND game_id = ? {exclude}""",
                (norm_team, game_id, *exclude_args),
            ).fetchone()
            if hit:
                errors.append("This Team Name / Nick Name is already taken — it already exists for this game.")
        except Exception:
            pass

    # --- 2. CNIC duplicate check ---
    # Removed as per user request to allow multiple registrations

    # --- 3. Player (email) already registered in this game ---
    # Removed as per user request to allow multiple registrations

    # --- 4. Team members already registered in this game ---
    # Removed as per user request to allow multiple registrations

    return errors


def field_duplicate_message(conn, field, value, game_id, selected_q_ids=None):
    """Return a user-facing duplicate message for a single field, or "" if free.

    Used by the live per-field check on the registration form. `field` must be
    one of: team_name, cnic, email. All checks are game-scoped.
    """
    if not value or not game_id:
        return ""
    try:
        game_id = int(game_id)
    except (ValueError, TypeError):
        return ""
    q_set = {int(x) for x in (selected_q_ids or []) if str(x).strip().isdigit()}
    has_q = bool(q_set)

    if field == "team_name":
        norm = (value or "").strip()
        if not norm:
            return ""
        hit = conn.execute(
            "SELECT id FROM registrations WHERE LOWER(TRIM(team_name)) = LOWER(?) AND game_id = ?",
            (norm, game_id),
        ).fetchone()
        if hit:
            return "This Team Name / Nick Name is already taken — it already exists for this game."
        return ""

    if field == "cnic":
        # Removed duplication check to allow multiple registrations
        return ""

    if field == "email":
        # Removed duplication check to allow multiple registrations
        return ""

    return ""
