import calendar
import uuid
import secrets
import string
import re

from datetime import datetime
from typing import Optional

from pydantic import BaseModel
from bson import ObjectId

from MongoDBConnection import db, users_collection
from controller.mail_helper import MailHelper
from model.access_token_model import AccessTokenModel
from utils.password_helper import hash_password, is_password_hash, verify_password


orders_collection = db["orders"]


# --- 1. SCHEMAS (Validate Input) ---
class UserGoogleLoginPayload(BaseModel):
    email: str
    firstName: str
    lastName: str
    avatarUrl: str


class UserLoginPayload(BaseModel):
    email: str
    password: str


class UserForgotPasswordPayload(BaseModel):
    email: str


class UserUpdatePasswordPayload(BaseModel):
    oldPassword: Optional[str] = None
    newPassword: str


class UserUpdateProfilePayload(BaseModel):
    # Để optional để frontend cũ nếu còn gửi email thì không lỗi.
    # Nhưng backend không dùng email này để xác định user nữa.
    email: Optional[str] = None
    firstName: Optional[str] = None
    lastName: Optional[str] = None
    phone: Optional[str] = None
    address: Optional[str] = None
    avatarUrl: Optional[str] = None


# --- 2. MODEL LOGIC (Xử lý DB & Nghiệp vụ) ---
class UserModel:
    @staticmethod
    def get_user_by_email(email: str):
        return users_collection.find_one({"email": email, "role": "user"})

    @staticmethod
    def get_user_by_id(user_id: str):
        try:
            return users_collection.find_one({"_id": ObjectId(user_id), "role": "user"})
        except Exception:
            return None

    @staticmethod
    def _safe_float(value, default=0.0):
        try:
            return float(value or 0)
        except (TypeError, ValueError):
            return float(default)

    @staticmethod
    def _calculate_order_total(items):
        if not isinstance(items, list):
            return 0.0

        total = 0.0
        for item in items:
            if not isinstance(item, dict):
                continue

            quantity = UserModel._safe_float(item.get("quantity"))
            price = UserModel._safe_float(item.get("price"))
            shipping = UserModel._safe_float(item.get("shipping"))
            tax = UserModel._safe_float(item.get("tax"))

            total += quantity * price + shipping + tax

        return round(total, 2)

    @staticmethod
    def _resolve_order_total(order_doc):
        total = order_doc.get("total")
        try:
            return round(float(total), 2)
        except (TypeError, ValueError):
            return UserModel._calculate_order_total(order_doc.get("items", []))

    @staticmethod
    def _current_month_range():
        now = datetime.utcnow()
        year = now.year
        month = now.month
        start = f"{year:04d}-{month:02d}-01"
        end_day = calendar.monthrange(year, month)[1]
        end = f"{year:04d}-{month:02d}-{end_day:02d}"
        return start, end

    @staticmethod
    def _order_date_key(order_doc):
        created_at = str(order_doc.get("createdAt") or "").strip()
        if created_at:
            return created_at[:10]

        date_value = str(order_doc.get("date") or "").strip()
        if len(date_value) >= 10:
            return date_value[:10]

        return date_value

    @staticmethod
    def _is_in_period(order_doc, date_from=None, date_to=None):
        period_from = (date_from or "").strip()
        period_to = (date_to or "").strip()

        if not period_from and not period_to:
            period_from, period_to = UserModel._current_month_range()

        order_date = UserModel._order_date_key(order_doc)
        if not order_date:
            return False

        if period_from and order_date < period_from:
            return False

        if period_to and order_date > period_to:
            return False

        return True

    @staticmethod
    def _is_admin_cancelled(order_doc):
        return (
            str(order_doc.get("status") or "").strip() == "Cancelled"
            and str(order_doc.get("cancelledBy") or "").strip() == "admin"
        )

    @staticmethod
    def _is_paid_like_order(order_doc):
        status = str(order_doc.get("status") or "").strip()
        return status in ["Processing", "Printing", "Shipped", "Delivered"]

    @staticmethod
    def search_users(
        keyword: str = "",
        page: int = 1,
        page_size: int = 10,
        date_from: str = None,
        date_to: str = None,
        sort_by: str = "balance",
        sort_order: str = "desc",
    ):
        page = max(1, int(page))
        page_size = max(1, min(100, int(page_size)))

        query = {"role": "user"}

        keyword = (keyword or "").strip()
        if keyword:
            escaped_keyword = re.escape(keyword)
            regex = {"$regex": escaped_keyword, "$options": "i"}

            query["$or"] = [
                {"email": regex},
                {"firstName": regex},
                {"lastName": regex},
                {"phone": regex},
                {"address": regex},
                {"city": regex},
                {"state": regex},
                {"country": regex},
                {"zipCode": regex},
            ]

        users = list(users_collection.find(query))

        user_ids = [str(user.get("_id")) for user in users if user.get("_id")]
        stats_map = {
            user_id: {
                "totalSpent": 0.0,
                "periodOrderCount": 0,
                "periodSpent": 0.0,
            }
            for user_id in user_ids
        }

        if user_ids:
            order_cursor = orders_collection.find(
                {"id_khach_hang": {"$in": user_ids}},
                {
                    "id_khach_hang": 1,
                    "status": 1,
                    "cancelledBy": 1,
                    "total": 1,
                    "items": 1,
                    "date": 1,
                    "createdAt": 1,
                },
            )

            for order in order_cursor:
                customer_id = str(order.get("id_khach_hang") or "").strip()
                if not customer_id or customer_id not in stats_map:
                    continue

                # Bỏ các đơn admin đã hủy
                if UserModel._is_admin_cancelled(order):
                    continue

                order_total = UserModel._resolve_order_total(order)
                is_paid_like = UserModel._is_paid_like_order(order)
                is_in_period = UserModel._is_in_period(order, date_from, date_to)

                # Tổng đã chi toàn thời gian: chỉ tính đơn đã thanh toán / đã vào xử lý
                if is_paid_like:
                    stats_map[customer_id]["totalSpent"] += order_total

                # Số đơn trong khoảng lọc + tiền trong khoảng lọc:
                # chỉ tính đơn đã thanh toán / đã vào xử lý
                if is_in_period and is_paid_like:
                    stats_map[customer_id]["periodOrderCount"] += 1
                    stats_map[customer_id]["periodSpent"] += order_total

        items = []
        for user in users:
            user_id = str(user.get("_id"))
            stats = stats_map.get(
                user_id,
                {
                    "totalSpent": 0.0,
                    "periodOrderCount": 0,
                    "periodSpent": 0.0,
                },
            )

            first_name = (user.get("firstName", "") or "").strip()
            last_name = (user.get("lastName", "") or "").strip()
            full_name = f"{first_name} {last_name}".strip()

            total_balance = UserModel._safe_float(user.get("balance", 0))
            debt_balance = UserModel._safe_float(user.get("debtBalance", 0))
            total_spent = round(UserModel._safe_float(stats.get("totalSpent", 0)), 2)
            period_order_count = int(stats.get("periodOrderCount", 0) or 0)
            period_spent = round(UserModel._safe_float(stats.get("periodSpent", 0)), 2)

            snapshot = UserModel.get_tier_snapshot(user_id)

            items.append(
                {
                    "id": user_id,
                    "name": full_name or user.get("email", ""),
                    "phone": user.get("phone", ""),
                    "email": user.get("email", ""),
                    "address": user.get("address", ""),
                    "city": user.get("city", ""),
                    "state": user.get("state", ""),
                    "country": user.get("country", ""),
                    "zipCode": user.get("zipCode", ""),
                    "createdAt": user.get("createdAt", ""),
                    "tierCurrent": snapshot["tierCurrent"],
                    "currentMonthPaidQty": snapshot["currentMonthPaidQty"],
                    "previousMonthPaidQty": snapshot["previousMonthPaidQty"],
                    "totalBalance": round(total_balance, 2),
                    "debtBalance": round(debt_balance, 2),
                    "totalSpent": total_spent,
                    "periodOrderCount": period_order_count,
                    "periodSpent": period_spent,
                    "spent": total_spent,
                    "monthOrderCount": period_order_count,
                    "monthSpent": period_spent,
                }
            )

        reverse = str(sort_order or "desc").lower() != "asc"

        if str(sort_by or "balance").lower() == "spent":
            items.sort(
                key=lambda x: (
                    UserModel._safe_float(x.get("totalSpent", 0)),
                    UserModel._safe_float(x.get("totalBalance", 0)),
                    str(x.get("name", "")).lower(),
                ),
                reverse=reverse,
            )
        elif str(sort_by or "balance").lower() == "debt":
            items.sort(key=lambda x: (UserModel._safe_float(x.get("debtBalance", 0)), UserModel._safe_float(x.get("totalBalance", 0))), reverse=reverse)
        else:
            items.sort(
                key=lambda x: (
                    UserModel._safe_float(x.get("totalBalance", 0)),
                    UserModel._safe_float(x.get("totalSpent", 0)),
                    str(x.get("name", "")).lower(),
                ),
                reverse=reverse,
            )

        total = len(items)
        total_pages = (total + page_size - 1) // page_size if total > 0 else 1
        skip = (page - 1) * page_size
        paged_items = items[skip: skip + page_size]

        return {
            "items": paged_items,
            "total": total,
            "page": page,
            "pageSize": page_size,
            "totalPages": total_pages,
        }

    @staticmethod
    def add_balance(user_id: str, amount: float):
        result = users_collection.update_one(
            {"_id": ObjectId(user_id), "role": "user"},
            {"$inc": {"balance": float(amount)}}
        )
        return result.modified_count > 0

    @staticmethod
    def deduct_balance(user_id: str, amount: float):
        amount = float(amount)

        if amount <= 0:
            raise ValueError("Số tiền trừ không hợp lệ")

        user = UserModel.get_user_by_id(user_id)
        if not user:
            raise ValueError("Không tìm thấy user")

        current_balance = float(user.get("balance", 0) or 0)
        if current_balance < amount:
            raise ValueError(f"Số dư không đủ. Hiện có ${current_balance:.2f}, cần ${amount:.2f}")

        result = users_collection.update_one(
            {"_id": ObjectId(user_id), "role": "user", "balance": {"$gte": amount}},
            {"$inc": {"balance": -amount}}
        )

        if result.modified_count == 0:
            raise ValueError("Không thể trừ số dư, vui lòng thử lại")

        return round(current_balance - amount, 2)

    @staticmethod
    def handle_google_login(payload: UserGoogleLoginPayload):
        user = UserModel.get_user_by_email(payload.email)
        if not user:
            from model.tier_settings_model import TierSettingsModel

            settings = TierSettingsModel.get_settings()
            default_tier = settings.get("defaultTier", "Silver")

            new_user = {
                "userId": str(uuid.uuid4()),
                "email": payload.email,
                "firstName": payload.firstName,
                "lastName": payload.lastName,
                "avatarUrl": payload.avatarUrl,
                "balance": 0,
                "debtBalance": 0,
                "auth_provider": "google",
                "password": "",
                "role": "user",
                "createdAt": datetime.utcnow().isoformat(),
                "tierCurrent": default_tier,
                "tierUpdatedAt": datetime.utcnow().isoformat(),
            }
            insert_result = users_collection.insert_one(new_user)
            new_user["_id"] = insert_result.inserted_id
            user = new_user

        patch = {}
        if not user.get("createdAt"):
            patch["createdAt"] = datetime.utcnow().isoformat()
        if not user.get("tierCurrent"):
            patch["tierCurrent"] = "Silver"
            patch["tierUpdatedAt"] = datetime.utcnow().isoformat()

        if patch:
            users_collection.update_one({"_id": user["_id"]}, {"$set": patch})
            user.update(patch)

        return user

    @staticmethod
    def verify_login(email, password):
        user = UserModel.get_user_by_email(email)
        if user and verify_password(password, user.get("password", "")):
            # Tự động nâng cấp password plaintext cũ sang hash sau khi login đúng.
            if not is_password_hash(user.get("password", "")):
                users_collection.update_one(
                    {"_id": user["_id"]},
                    {"$set": {"password": hash_password(password)}}
                )
            return user
        return None

    @staticmethod
    def update_profile(
        email: str,
        first_name: Optional[str] = None,
        last_name: Optional[str] = None,
        phone: Optional[str] = None,
        address: Optional[str] = None,
        avatar_url: Optional[str] = None,
    ):
        """
        Giữ lại hàm cũ để tránh lỗi nếu chỗ khác trong code còn gọi.
        Nhưng controller mới nên dùng update_profile_by_id().
        """
        user = UserModel.get_user_by_email(email)
        if not user:
            return None

        update_fields = {}

        if first_name is not None:
            update_fields["firstName"] = str(first_name).strip()

        if last_name is not None:
            update_fields["lastName"] = str(last_name).strip()

        if phone is not None:
            update_fields["phone"] = str(phone).strip()

        if address is not None:
            update_fields["address"] = str(address).strip()

        if avatar_url is not None:
            update_fields["avatarUrl"] = avatar_url

        if update_fields:
            users_collection.update_one(
                {"email": email, "role": "user"},
                {"$set": update_fields}
            )

        return UserModel.get_user_by_email(email)

    @staticmethod
    def update_profile_by_id(
        user_id: str,
        first_name: Optional[str] = None,
        last_name: Optional[str] = None,
        phone: Optional[str] = None,
        address: Optional[str] = None,
        avatar_url: Optional[str] = None,
    ):
        user = UserModel.get_user_by_id(user_id)
        if not user:
            return None

        update_fields = {}

        if first_name is not None:
            update_fields["firstName"] = str(first_name).strip()

        if last_name is not None:
            update_fields["lastName"] = str(last_name).strip()

        if phone is not None:
            update_fields["phone"] = str(phone).strip()

        if address is not None:
            update_fields["address"] = str(address).strip()

        if avatar_url is not None:
            update_fields["avatarUrl"] = avatar_url

        if update_fields:
            users_collection.update_one(
                {"_id": user["_id"], "role": "user"},
                {"$set": update_fields}
            )

        return UserModel.get_user_by_id(user_id)

    @staticmethod
    def reset_password(email):
        user = UserModel.get_user_by_email(email)
        if not user:
            return False, "Email không tồn tại trên hệ thống!"

        alphabet = string.ascii_letters + string.digits
        random_password = ''.join(secrets.choice(alphabet) for _ in range(8))

        users_collection.update_one(
            {"_id": user["_id"], "role": "user"},
            {"$set": {"password": hash_password(random_password)}}
        )

        # Reset password thì revoke toàn bộ token cũ của user.
        AccessTokenModel.revoke_all_for_actor(str(user.get("_id")))

        email_data = {
            "username": f"{user.get('firstName')} {user.get('lastName')}",
            "password": random_password,
            "note": "Vì lý do bảo mật, hãy đổi lại mật khẩu này ngay sau khi đăng nhập thành công."
        }

        mail_sent = MailHelper.send_new_password_email(email, email_data)
        if mail_sent:
            return True, "Mật khẩu mới đã được gửi vào Email của bạn!"
        return False, "Lỗi khi gửi email."

    @staticmethod
    def update_password(email, new_password):
        """
        Giữ lại hàm cũ để tránh lỗi nếu chỗ khác trong code còn gọi.
        Controller mới nên dùng update_password_by_id().
        """
        user = UserModel.get_user_by_email(email)
        if not user:
            return False

        result = users_collection.update_one(
            {"_id": user["_id"], "role": "user"},
            {"$set": {"password": hash_password(new_password)}}
        )

        if result.modified_count > 0:
            AccessTokenModel.revoke_all_for_actor(str(user.get("_id")))

        return result.modified_count > 0

    @staticmethod
    def update_password_by_id(user_id: str, old_password: str, new_password: str):
        user = UserModel.get_user_by_id(user_id)
        if not user:
            return False, "Không tìm thấy user"

        stored_password = user.get("password", "")

        # Nếu user có password thì bắt buộc nhập đúng mật khẩu cũ.
        # Với user Google chưa có password, stored_password có thể rỗng.
        if stored_password and not verify_password(old_password, stored_password):
            return False, "Mật khẩu hiện tại không chính xác"

        result = users_collection.update_one(
            {"_id": user["_id"], "role": "user"},
            {"$set": {"password": hash_password(new_password)}}
        )

        if result.modified_count > 0:
            AccessTokenModel.revoke_all_for_actor(str(user.get("_id")))
            return True, "Cập nhật thành công, vui lòng đăng nhập lại"

        return False, "Không thể cập nhật mật khẩu"

    @staticmethod
    def _month_range(year: int, month: int):
        start = datetime(year, month, 1)
        if month == 12:
            end = datetime(year + 1, 1, 1)
        else:
            end = datetime(year, month + 1, 1)
        return start, end

    @staticmethod
    def _shift_month(year: int, month: int, offset: int):
        total = year * 12 + (month - 1) + offset
        new_year = total // 12
        new_month = total % 12 + 1
        return new_year, new_month

    @staticmethod
    def _count_paid_items_in_month(user_id: str, year: int, month: int) -> int:
        start, end = UserModel._month_range(year, month)

        cursor = orders_collection.find(
            {
                "id_khach_hang": user_id,
                "status": {"$in": ["Processing", "Printing", "Shipped", "Delivered"]},
                "createdAt": {
                    "$gte": start.isoformat(),
                    "$lt": end.isoformat(),
                },
            },
            {"items": 1, "createdAt": 1, "status": 1},
        )

        total_qty = 0
        for order in cursor:
            for item in order.get("items", []) or []:
                try:
                    total_qty += int(float(item.get("quantity", 0) or 0))
                except Exception:
                    pass

        return total_qty

    @staticmethod
    def _resolve_tier_from_qty(paid_qty: int, tiers: list, default_tier: str) -> str:
        matched = default_tier
        sorted_tiers = sorted(tiers, key=lambda x: (int(x.get("order", 0)), int(x.get("minPaidQty", 0))))
        for item in sorted_tiers:
            if paid_qty >= int(item.get("minPaidQty", 0)):
                matched = item.get("code", matched)
        return matched

    @staticmethod
    def _tier_rank_map(tiers: list) -> dict:
        result = {}
        for item in tiers:
            result[item["code"]] = int(item.get("order", 0))
        return result

    @staticmethod
    def get_tier_snapshot(user_id: str, now: datetime = None) -> dict:
        from model.tier_settings_model import TierSettingsModel

        now = now or datetime.utcnow()
        settings = TierSettingsModel.get_settings()
        tiers = settings.get("tiers", [])
        default_tier = settings.get("defaultTier", "Silver")
        rank_map = UserModel._tier_rank_map(tiers)

        curr_year = now.year
        curr_month = now.month
        prev_year, prev_month = UserModel._shift_month(curr_year, curr_month, -1)

        prev_qty = UserModel._count_paid_items_in_month(user_id, prev_year, prev_month)
        curr_qty = UserModel._count_paid_items_in_month(user_id, curr_year, curr_month)

        baseline_tier = UserModel._resolve_tier_from_qty(prev_qty, tiers, default_tier)
        current_month_tier = UserModel._resolve_tier_from_qty(curr_qty, tiers, default_tier)

        baseline_rank = rank_map.get(baseline_tier, 0)
        current_rank = rank_map.get(current_month_tier, 0)

        effective_tier = baseline_tier if baseline_rank >= current_rank else current_month_tier

        return {
            "defaultTier": default_tier,
            "tiers": tiers,
            "previousMonthPaidQty": prev_qty,
            "currentMonthPaidQty": curr_qty,
            "baselineTier": baseline_tier,
            "currentMonthTier": current_month_tier,
            "tierCurrent": effective_tier,
        }

    @staticmethod
    def sync_user_tier(user_id: str) -> dict:
        user = UserModel.get_user_by_id(user_id)
        if not user:
            raise ValueError("User not found")

        snapshot = UserModel.get_tier_snapshot(user_id)

        users_collection.update_one(
            {"_id": ObjectId(user_id), "role": "user"},
            {
                "$set": {
                    "tierCurrent": snapshot["tierCurrent"],
                    "tierUpdatedAt": datetime.utcnow().isoformat(),
                }
            }
        )

        return snapshot
