# handlers/user/start.py

from __future__ import annotations

import asyncio
import logging
import re
import sqlite3
from dataclasses import dataclass
from html import escape
from typing import Any, Final, Optional

from aiogram import F, Router
from aiogram.exceptions import (
    TelegramBadRequest,
    TelegramForbiddenError,
)
from aiogram.filters import CommandStart
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message, User

from core.config import settings
from core.ui_emojis import (
    ce,
    premiumize_html,
)
from keyboards.user_kb import get_user_main_menu


logger = logging.getLogger(__name__)

router = Router(
    name="user_start"
)


# ============================================================
# Constants
# ============================================================

MAX_SQLITE_INT: Final[int] = (
    9_223_372_036_854_775_807
)

_REFERRAL_PAYLOAD_RE: Final[
    re.Pattern[str]
] = re.compile(
    r"^ref_(\d+)$",
    flags=re.IGNORECASE,
)


# ============================================================
# Welcome Text
# ============================================================

START_WELCOME_TEXT: Final[str] = (
    f"{ce('shop', '🏠')} "
    "<b>منوی اصلی</b>\n"
    "━━━━━━━━━━━━━━━━━━━━\n\n"

    f"{ce('hello')} "
    "<b>خوش آمدید!</b>\n\n"

    "از طریق منوی زیر می‌توانید به فروشگاه، کیف پول، "
    "حساب کاربری، زیرمجموعه‌گیری و پیگیری سفارش‌های خود "
    "دسترسی داشته باشید.\n\n"

    f"{ce('target', '👇')} "
    "<i>لطفاً یکی از گزینه‌های زیر را انتخاب کنید.</i>"
)


# ============================================================
# Result Model
# ============================================================

@dataclass(slots=True)
class StartRegistrationResult:
    success: bool

    is_new_user: bool = False

    referral_created: bool = False

    referrer_id: Optional[int] = None

    referral_reason: str = "none"

    error: Optional[str] = None


# ============================================================
# Database Path
# ============================================================

def resolve_database_path(
    db: Any = None,
) -> str:
    """
    اول از DatabaseManager اصلی Dispatcher مسیر را می‌گیریم.

    fallback:
        settings.DB_PATH
        settings.DATABASE_PATH
        matrix_bot.db
    """
    if db is not None:
        for attribute in (
            "db_path",
            "database_path",
            "path",
        ):
            value = getattr(
                db,
                attribute,
                None,
            )

            if value:
                return str(
                    value
                )

    value = (
        getattr(
            settings,
            "DB_PATH",
            None,
        )
        or getattr(
            settings,
            "DATABASE_PATH",
            None,
        )
        or "matrix_bot.db"
    )

    return str(
        value
    )


# ============================================================
# SQLite Helpers
# ============================================================

def _connect(
    db_path: str,
) -> sqlite3.Connection:
    conn = sqlite3.connect(
        db_path,
        timeout=30,
        check_same_thread=False,
    )

    conn.row_factory = (
        sqlite3.Row
    )

    conn.execute(
        "PRAGMA busy_timeout = 30000"
    )

    # در DB فعلی پروژه یک FK قدیمی در orders ممکن است به
    # users(id) اشاره کند، در حالی که کلید اصلی users.user_id است.
    # این اتصال فقط users/referrals را مدیریت می‌کند و اعتبار
    # Referral را داخل Transaction به‌صورت دستی بررسی می‌کند؛
    # بنابراین FK enforcement این connection عمداً خاموش است تا
    # یک FK خراب و نامرتبط، /start را از کار نیندازد.
    conn.execute(
        "PRAGMA foreign_keys = OFF"
    )

    return conn


def _table_exists(
    conn: sqlite3.Connection,
    table_name: str,
) -> bool:
    row = conn.execute(
        """
        SELECT 1
        FROM sqlite_master
        WHERE type = 'table'
          AND name = ?
        LIMIT 1
        """,
        (
            table_name,
        ),
    ).fetchone()

    return row is not None


def _column_names(
    conn: sqlite3.Connection,
    table_name: str,
) -> set[str]:
    if not _table_exists(
        conn,
        table_name,
    ):
        return set()

    return {
        str(
            row["name"]
        )
        for row in conn.execute(
            f"PRAGMA table_info({table_name})"
        ).fetchall()
    }


def _ensure_referral_indexes_locked(
    conn: sqlite3.Connection,
) -> None:
    if not _table_exists(
        conn,
        "referrals",
    ):
        return

    columns = _column_names(
        conn,
        "referrals",
    )

    if "referrer_id" in columns:
        conn.execute(
            """
            CREATE INDEX IF NOT EXISTS
            idx_referrals_referrer
            ON referrals(referrer_id)
            """
        )

    if "referred_id" in columns:
        conn.execute(
            """
            CREATE INDEX IF NOT EXISTS
            idx_referrals_referred
            ON referrals(referred_id)
            """
        )


# ============================================================
# Referral Parsing
# ============================================================

def extract_referrer_id(
    message: Message,
) -> Optional[int]:
    """
    نمونه:
        /start ref_123456789

    فقط payload دقیق ref_<positive integer> قبول می‌شود.
    """
    if (
        not message.text
        or not message.from_user
    ):
        return None

    parts = (
        message.text
        .strip()
        .split(
            maxsplit=1
        )
    )

    if len(
        parts
    ) != 2:
        return None

    payload = (
        parts[1]
        .strip()
    )

    match = (
        _REFERRAL_PAYLOAD_RE
        .fullmatch(
            payload
        )
    )

    if not match:
        return None

    try:
        referrer_id = int(
            match.group(1)
        )

    except (
        TypeError,
        ValueError,
    ):
        return None

    if (
        referrer_id <= 0
        or referrer_id
        > MAX_SQLITE_INT
    ):
        return None

    if (
        referrer_id
        == int(
            message.from_user.id
        )
    ):
        return None

    return referrer_id


# ============================================================
# User / Referral Persistence
# ============================================================

def _upsert_identity_locked(
    conn: sqlite3.Connection,
    *,
    user: User,
    is_new_user: bool,
    valid_referrer_id: Optional[int],
) -> None:
    """
    با introspection با چند نسخه Schema سازگار می‌ماند.
    """
    columns = _column_names(
        conn,
        "users",
    )

    if "user_id" not in columns:
        raise RuntimeError(
            "users.user_id column is missing."
        )

    user_id = int(
        user.id
    )

    identity_values: dict[
        str,
        Any,
    ] = {
        "username":
            user.username
            or None,
        "full_name":
            user.full_name
            or None,
        "first_name":
            user.first_name
            or None,
        "last_name":
            user.last_name
            or None,
    }

    if is_new_user:
        insert_columns: list[str] = [
            "user_id"
        ]

        insert_values: list[Any] = [
            user_id
        ]

        for column, value in (
            identity_values.items()
        ):
            if column in columns:
                insert_columns.append(
                    column
                )
                insert_values.append(
                    value
                )

        if (
            valid_referrer_id
            is not None
            and "invited_by"
            in columns
        ):
            insert_columns.append(
                "invited_by"
            )
            insert_values.append(
                valid_referrer_id
            )

        placeholders = ", ".join(
            "?"
            for _
            in insert_columns
        )

        conn.execute(
            (
                "INSERT INTO users ("
                + ", ".join(
                    insert_columns
                )
                + ") VALUES ("
                + placeholders
                + ")"
            ),
            tuple(
                insert_values
            ),
        )

        return

    assignments: list[str] = []
    params: list[Any] = []

    for column, value in (
        identity_values.items()
    ):
        if column in columns:
            assignments.append(
                f"{column} = ?"
            )
            params.append(
                value
            )

    if "updated_at" in columns:
        assignments.append(
            "updated_at = CURRENT_TIMESTAMP"
        )

    if assignments:
        params.append(
            user_id
        )

        conn.execute(
            (
                "UPDATE users SET "
                + ", ".join(
                    assignments
                )
                + " WHERE user_id = ?"
            ),
            tuple(
                params
            ),
        )


def _validate_referrer_locked(
    conn: sqlite3.Connection,
    *,
    user_id: int,
    referrer_id: Optional[int],
) -> tuple[
    Optional[int],
    str,
]:
    if referrer_id is None:
        return (
            None,
            "no_referrer",
        )

    if referrer_id == user_id:
        return (
            None,
            "self_referral",
        )

    referrer = conn.execute(
        """
        SELECT *
        FROM users
        WHERE user_id = ?
        LIMIT 1
        """,
        (
            referrer_id,
        ),
    ).fetchone()

    if not referrer:
        return (
            None,
            "referrer_not_found",
        )

    referrer_data = dict(
        referrer
    )

    # اگر Schema وضعیت مسدودی دارد، کاربر مسدودشده
    # معرف جدید دریافت نمی‌کند.
    blocked = False

    for key in (
        "is_banned",
        "is_blocked",
    ):
        if key in referrer_data:
            try:
                blocked = (
                    int(
                        referrer_data[
                            key
                        ]
                        or 0
                    )
                    == 1
                )
            except (
                TypeError,
                ValueError,
            ):
                blocked = False

            if blocked:
                break

    if blocked:
        return (
            None,
            "referrer_blocked",
        )

    return (
        referrer_id,
        "valid",
    )


def _create_referral_locked(
    conn: sqlite3.Connection,
    *,
    referrer_id: int,
    referred_id: int,
) -> bool:
    if not _table_exists(
        conn,
        "referrals",
    ):
        logger.warning(
            "referrals table is missing; "
            "referral relationship was not created."
        )
        return False

    columns = _column_names(
        conn,
        "referrals",
    )

    required = {
        "referrer_id",
        "referred_id",
    }

    if not required.issubset(
        columns
    ):
        logger.warning(
            "referrals schema is incompatible."
        )
        return False

    # هر کاربر فقط یک معرف.
    existing = conn.execute(
        """
        SELECT 1
        FROM referrals
        WHERE referred_id = ?
        LIMIT 1
        """,
        (
            referred_id,
        ),
    ).fetchone()

    if existing:
        return False

    insert_columns = [
        "referrer_id",
        "referred_id",
    ]

    values: list[Any] = [
        referrer_id,
        referred_id,
    ]

    if "reward_earned" in columns:
        insert_columns.append(
            "reward_earned"
        )
        values.append(
            0
        )

    placeholders = ", ".join(
        "?"
        for _
        in insert_columns
    )

    conn.execute(
        (
            "INSERT INTO referrals ("
            + ", ".join(
                insert_columns
            )
            + ") VALUES ("
            + placeholders
            + ")"
        ),
        tuple(
            values
        ),
    )

    return True


def _register_start_sync(
    *,
    db_path: str,
    user: User,
    referrer_id: Optional[int],
) -> StartRegistrationResult:
    user_id = int(
        user.id
    )

    conn = _connect(
        db_path
    )

    try:
        if not _table_exists(
            conn,
            "users",
        ):
            return StartRegistrationResult(
                success=False,
                error=(
                    "users table is missing"
                ),
                referral_reason=(
                    "database_not_initialized"
                ),
            )

        conn.execute(
            "BEGIN IMMEDIATE"
        )

        _ensure_referral_indexes_locked(
            conn
        )

        existing_user = conn.execute(
            """
            SELECT 1
            FROM users
            WHERE user_id = ?
            LIMIT 1
            """,
            (
                user_id,
            ),
        ).fetchone()

        is_new_user = (
            existing_user
            is None
        )

        valid_referrer_id: Optional[
            int
        ] = None

        referral_reason = (
            "existing_user"
            if not is_new_user
            else "no_referrer"
        )

        # Referral فقط هنگام اولین ثبت کاربر پردازش می‌شود.
        if is_new_user:
            (
                valid_referrer_id,
                referral_reason,
            ) = _validate_referrer_locked(
                conn,
                user_id=user_id,
                referrer_id=referrer_id,
            )

        _upsert_identity_locked(
            conn,
            user=user,
            is_new_user=is_new_user,
            valid_referrer_id=(
                valid_referrer_id
            ),
        )

        referral_created = False

        if (
            is_new_user
            and valid_referrer_id
            is not None
        ):
            referral_created = (
                _create_referral_locked(
                    conn,
                    referrer_id=(
                        valid_referrer_id
                    ),
                    referred_id=user_id,
                )
            )

            referral_reason = (
                "created"
                if referral_created
                else "not_created"
            )

        conn.commit()

        return StartRegistrationResult(
            success=True,
            is_new_user=is_new_user,
            referral_created=(
                referral_created
            ),
            referrer_id=(
                valid_referrer_id
            ),
            referral_reason=(
                referral_reason
            ),
        )

    except Exception as exc:
        try:
            conn.rollback()
        except Exception:
            pass

        logger.exception(
            "Atomic /start registration failed | "
            "user=%s",
            user_id,
        )

        return StartRegistrationResult(
            success=False,
            error=(
                f"{type(exc).__name__}: "
                f"{exc}"
            ),
            referral_reason=(
                "database_error"
            ),
        )

    finally:
        conn.close()


async def register_user_start(
    *,
    user: User,
    referrer_id: Optional[int],
    db: Any = None,
) -> StartRegistrationResult:
    """
    عملیات SQLite داخل Thread اجرا می‌شود تا Event Loop
    تلگرام block نشود.

    این مسیر عمداً به DatabaseManager.create_tables() یا
    upsert_user_activity() وابسته نیست.
    """
    db_path = (
        resolve_database_path(
            db
        )
    )

    return await asyncio.to_thread(
        _register_start_sync,
        db_path=db_path,
        user=user,
        referrer_id=referrer_id,
    )


# ============================================================
# Telegram Helpers
# ============================================================

async def safe_callback_answer(
    callback: CallbackQuery,
    text: Optional[str] = None,
    *,
    show_alert: bool = False,
) -> bool:
    try:
        await callback.answer(
            text=text,
            show_alert=show_alert,
        )

        return True

    except TelegramBadRequest:
        return False

    except Exception:
        logger.exception(
            "Could not answer main-menu callback."
        )

        return False


async def show_main_menu(
    message: Message,
) -> bool:
    """
    ارسال منوی اصلی به صورت پیام جدید.
    """
    try:
        await message.answer(
            premiumize_html(
                START_WELCOME_TEXT
            ),
            reply_markup=(
                get_user_main_menu(
                    message.from_user.id
                    if message.from_user
                    else None
                )
            ),
            parse_mode="HTML",
        )

        return True

    except TelegramForbiddenError:
        logger.warning(
            "User blocked the bot | user=%s",
            (
                message.from_user.id
                if message.from_user
                else None
            ),
        )

        return False

    except Exception:
        logger.exception(
            "Could not send main menu | user=%s",
            (
                message.from_user.id
                if message.from_user
                else None
            ),
        )

        return False


async def show_main_menu_from_callback(
    callback: CallbackQuery,
) -> bool:
    """
    اگر پیام فعلی Text باشد Edit می‌شود.

    اگر callback از روی Photo/Media آمده باشد (مثلاً کارت پروفایل)،
    یک پیام منوی جدید ارسال و Media قبلی حذف می‌شود؛ چون edit_text
    روی پیام Media قابل اتکا نیست.
    """
    message = callback.message

    if not message:
        return False

    rendered = premiumize_html(
        START_WELCOME_TEXT
    )

    keyboard = (
        get_user_main_menu(
            callback.from_user.id
            if callback.from_user
            else None
        )
    )

    # --------------------------------------------------------
    # Normal text message
    # --------------------------------------------------------

    if message.text is not None:
        try:
            await message.edit_text(
                rendered,
                reply_markup=keyboard,
                parse_mode="HTML",
            )

            return True

        except TelegramBadRequest as exc:
            error = str(
                exc
            ).lower()

            if (
                "message is not modified"
                in error
            ):
                return True

            if (
                "message can't be edited"
                not in error
                and "message to edit not found"
                not in error
            ):
                logger.warning(
                    "Could not edit main menu: %s",
                    exc,
                )

        except Exception:
            logger.exception(
                "Unexpected main-menu edit failure."
            )

    # --------------------------------------------------------
    # Media / uneditable fallback
    # --------------------------------------------------------

    try:
        await message.answer(
            rendered,
            reply_markup=keyboard,
            parse_mode="HTML",
        )

    except Exception:
        logger.exception(
            "Could not send replacement main menu."
        )
        return False

    # پیام قبلی فقط بعد از ارسال موفق حذف می‌شود.
    try:
        await message.delete()

    except TelegramBadRequest:
        pass

    except Exception:
        logger.warning(
            "Could not delete previous menu/media message.",
            exc_info=True,
        )

    return True


# ============================================================
# /start
# ============================================================

@router.message(
    CommandStart()
)
async def cmd_start(
    message: Message,
    state: FSMContext,
    db: Any = None,
) -> None:
    """
    ورود کاربر.

    نکات:
        - DatabaseManager جدید داخل این فایل ساخته نمی‌شود.
        - create_tables روی هر /start اجرا نمی‌شود.
        - Referral فقط هنگام اولین ثبت کاربر قابل ثبت است.
        - خطای DB مانع باز شدن منوی اصلی نمی‌شود.
    """
    await state.clear()

    user = (
        message.from_user
    )

    if not user:
        return

    user_id = int(
        user.id
    )

    referrer_id = (
        extract_referrer_id(
            message
        )
    )

    registration: Optional[
        StartRegistrationResult
    ] = None

    try:
        registration = (
            await register_user_start(
                user=user,
                referrer_id=(
                    referrer_id
                ),
                db=db,
            )
        )

        if registration.success:
            logger.info(
                "User start registered | "
                "user=%s new=%s referrer=%s "
                "referral_created=%s reason=%s",
                user_id,
                registration.is_new_user,
                registration.referrer_id,
                registration.referral_created,
                registration.referral_reason,
            )

        else:
            logger.error(
                "User start DB registration failed | "
                "user=%s reason=%s error=%s",
                user_id,
                registration.referral_reason,
                registration.error,
            )

    except Exception:
        # هیچ خطای DB نباید /start UI را متوقف کند.
        logger.exception(
            "Unexpected /start persistence error | "
            "user=%s",
            user_id,
        )

    sent = await show_main_menu(
        message
    )

    if sent:
        return

    # آخرین fallback؛ عمداً بدون Custom Emoji entity.
    try:
        await message.answer(
            "❌ خطا در بارگذاری منوی اصلی.\n"
            "لطفاً دوباره تلاش کنید."
        )

    except Exception:
        logger.exception(
            "Failed to send /start fallback | "
            "user=%s",
            user_id,
        )


# ============================================================
# Main Menu Callback
# ============================================================

@router.callback_query(
    F.data == "main_menu"
)
async def callback_main_menu(
    callback: CallbackQuery,
    state: FSMContext,
) -> None:
    """
    بازگشت امن به منوی اصلی از Text یا Media.
    """
    await state.clear()

    if not callback.message:
        await safe_callback_answer(
            callback
        )
        return

    user_id = (
        callback.from_user.id
        if callback.from_user
        else None
    )

    try:
        success = (
            await show_main_menu_from_callback(
                callback
            )
        )

        if success:
            await safe_callback_answer(
                callback
            )
            return

        await safe_callback_answer(
            callback,
            "❌ خطا در بارگذاری منوی اصلی.",
            show_alert=True,
        )

    except Exception:
        logger.exception(
            "Main-menu callback failed | "
            "user=%s",
            user_id,
        )

        await safe_callback_answer(
            callback,
            "❌ خطا در بارگذاری منوی اصلی.",
            show_alert=True,
        )


# ============================================================
# Exports
# ============================================================

__all__ = [
    "router",
    "START_WELCOME_TEXT",
    "StartRegistrationResult",
    "extract_referrer_id",
    "register_user_start",
    "show_main_menu",
    "callback_main_menu",
]
