from flask import Flask, has_app_context
import jwt
import time
import string
import random
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from flaskr.i18n import _

from ..common.models import raise_error, raise_param_error
from flaskr.common.cache_provider import cache as redis
from ...dao import db
from flaskr.api.sms.aliyun import send_sms_code_ali
from flaskr.service.user.captcha import consume_captcha_ticket
from .models import UserVerifyCode

import json

from flaskr.service.config.funcs import get_config as get_dynamic_config
from flaskr.service.shifu.models import AiCourseAuth, DraftShifu, PublishedShifu
from flaskr.service.common.phone_numbers import normalize_phone_identifier
from flaskr.service.user.repository import get_user_entity_by_bid, mark_user_roles
from flaskr.service.user.token_store import token_store
from flaskr.util import generate_id


def _normalize_language_code(language_code: str) -> str:
    """Normalize legacy or inconsistent language codes into a canonical form."""
    if not language_code:
        return ""

    normalized = language_code.replace("_", "-")
    parts = [segment for segment in normalized.split("-") if segment]

    if not parts:
        return ""

    primary = parts[0].lower()
    subtags = []

    for segment in parts[1:]:
        if len(segment) == 2 and segment.isalpha():
            subtags.append(segment.upper())
        elif len(segment) == 4 and segment.isalpha():
            subtags.append(segment.title())
        else:
            subtags.append(segment)

    normalized_parts = [primary]
    normalized_parts.extend(subtags)
    return "-".join(normalized_parts)


def get_user_language(user):
    language = ""
    if hasattr(user, "user_language") and user.user_language:
        language = user.user_language
    elif hasattr(user, "language") and user.language:
        language = user.language

    if language:
        # Return the user's language as-is, let the i18n system handle fallback
        # Only normalize old format for compatibility
        normalized = _normalize_language_code(language)
        if normalized:
            return normalized
        return language

    # No language set, default to English
    return "en-US"


def mark_creator_role_if_needed(user_id: str) -> bool:
    """Mark an existing user as creator and report whether this is a new grant."""

    normalized_user_id = str(user_id or "").strip()
    if not normalized_user_id:
        return False

    entity = get_user_entity_by_bid(normalized_user_id)
    if entity is None:
        return False
    if bool(entity.is_creator):
        return False

    mark_user_roles(normalized_user_id, is_creator=True)
    return True


def run_creator_granted_post_auth(
    app: Flask,
    *,
    user_id: str,
    source: str,
    login_context: str | None = None,
    created_new_user: bool = False,
    language: str | None = None,
) -> None:
    """Run post-auth hooks for flows that grant creator access outside login."""

    normalized_user_id = str(user_id or "").strip()
    if not normalized_user_id:
        return

    from flaskr.service.user.post_auth import PostAuthContext, run_post_auth_extensions

    run_post_auth_extensions(
        app,
        PostAuthContext(
            user_id=normalized_user_id,
            source=source,
            login_context=login_context,
            created_new_user=created_new_user,
            language=language,
            creator_granted_now=True,
        ),
    )


# generate token
def generate_token(app: Flask, user_id: str) -> str:
    def _generate() -> str:
        token = jwt.encode(
            {"user_id": user_id, "time_stamp": time.time()},
            app.config["SECRET_KEY"],
            algorithm="HS256",
        )
        token_store.save(
            app,
            user_id=user_id,
            token=token,
            ttl_seconds=app.config["TOKEN_EXPIRE_TIME"],
        )
        return token

    if has_app_context():
        return _generate()
    with app.app_context():
        return _generate()


# send sms code
def send_sms_code(
    app: Flask,
    phone: str,
    ip: str = None,
    captcha_ticket: str = None,
    require_captcha: bool = True,
):
    phone = normalize_phone_identifier(phone)
    with app.app_context():
        if not phone:
            raise_param_error("mobile")
        if require_captcha:
            consume_captcha_ticket(app, captcha_ticket)

        # Check IP ban status
        if ip:
            ip_ban_key = app.config["REDIS_KEY_PREFIX_IP_BAN"] + ip
            if redis.get(ip_ban_key):
                # Development, debugging and use
                # redis.delete(ip_ban_key)
                raise_error("server.user.ipBanned")

            # Check IP sending frequency
            ip_limit_key = app.config["REDIS_KEY_PREFIX_IP_LIMIT"] + ip
            ip_send_count = redis.get(ip_limit_key)

            if ip_send_count:
                ip_send_count = int(ip_send_count)
                if ip_send_count >= int(app.config["IP_SMS_LIMIT_COUNT"]):
                    # Ban the IP
                    redis.set(ip_ban_key, 1, ex=int(app.config["IP_BAN_TIME"]))
                    raise_error("server.user.ipBanned")
                else:
                    redis.incr(ip_limit_key)
            else:
                redis.set(ip_limit_key, 1, ex=int(app.config["IP_SMS_LIMIT_TIME"]))

        # Check phone sending frequency limit
        phone_limit_key = app.config["REDIS_KEY_PREFIX_PHONE_LIMIT"] + phone
        last_send_time = redis.get(phone_limit_key)

        if last_send_time:
            last_send_time = int(last_send_time)
            current_time = int(time.time())
            time_diff = current_time - last_send_time

            interval = int(app.config["SMS_CODE_INTERVAL"])
            if time_diff < interval:
                raise_error("server.user.smsSendTooFrequent")

        characters = string.digits
        # Generate a random string of length 4
        random_string = "".join(random.choices(characters, k=4))
        # 发送短信验证码
        redis.set(
            app.config["REDIS_KEY_PREFIX_PHONE_CODE"] + phone,
            random_string,
            ex=app.config["PHONE_CODE_EXPIRE_TIME"],
        )

        # Record the sending time
        redis.set(
            phone_limit_key, int(time.time()), ex=int(app.config["SMS_CODE_INTERVAL"])
        )

        user_verify_code = create_and_commit_user_verify_code(
            mail=None,
            phone=phone,
            verify_code=random_string,
            verify_code_type=1,  # 1: SMS, 2: Email
            ip=ip,
        )

        send_res = send_sms_code_ali(app, phone, random_string)
        if send_res:
            user_verify_code.verify_code_send = 1
            db.session.commit()
        return {"expire_in": app.config["PHONE_CODE_EXPIRE_TIME"]}


def send_email_code(app: Flask, email: str, ip: str = None, language: str = None):
    with app.app_context():
        email = str(email or "").strip().lower()
        if not email:
            raise_error("server.common.unknownError")

        # Check IP ban status
        if ip:
            ip_ban_key = app.config["REDIS_KEY_PREFIX_IP_BAN"] + ip
            if redis.get(ip_ban_key):
                # Development, debugging and use
                # redis.delete(ip_ban_key)
                raise_error("server.user.ipBanned")

            # Check IP sending frequency
            ip_limit_key = app.config["REDIS_KEY_PREFIX_IP_LIMIT"] + ip
            ip_send_count = redis.get(ip_limit_key)

            if ip_send_count:
                ip_send_count = int(ip_send_count)
                if ip_send_count >= int(app.config["IP_MAIL_LIMIT_COUNT"]):
                    # Ban the IP
                    redis.set(ip_ban_key, 1, ex=int(app.config["IP_BAN_TIME"]))
                    raise_error("server.user.ipBanned")
                else:
                    redis.incr(ip_limit_key)
            else:
                redis.set(ip_limit_key, 1, ex=int(app.config["IP_MAIL_LIMIT_TIME"]))

        # Check the transmission frequency limit
        email_limit_key = app.config["REDIS_KEY_PREFIX_MAIL_LIMIT"] + email
        last_send_time = redis.get(email_limit_key)

        if last_send_time:
            last_send_time = int(last_send_time)
            current_time = int(time.time())
            time_diff = current_time - last_send_time

            interval = int(app.config["MAIL_CODE_INTERVAL"])
            if time_diff < interval:
                raise_error("server.user.emailSendTooFrequent")

        # Create the email content
        msg = MIMEMultipart()
        msg["From"] = app.config["SMTP_SENDER"]
        msg["To"] = email
        msg["Subject"] = _("server.user.emailVerificationSubject")
        characters = string.digits
        random_string = "".join(random.choices(characters, k=4))
        # to set redis
        redis.set(
            app.config["REDIS_KEY_PREFIX_MAIL_CODE"] + email,
            random_string,
            ex=app.config["MAIL_CODE_EXPIRE_TIME"],
        )

        # Record the sending time of this time
        redis.set(
            email_limit_key, int(time.time()), ex=int(app.config["MAIL_CODE_INTERVAL"])
        )

        body = f"Your verification code is: {random_string}"
        msg.attach(MIMEText(body, "plain"))

        user_verify_code = create_and_commit_user_verify_code(
            mail=email,
            phone=None,
            verify_code=random_string,
            verify_code_type=2,  # 1: SMS, 2: Email
            ip=ip,
        )

        try:
            # Connect to the SMTP server
            server = smtplib.SMTP(app.config["SMTP_SERVER"], app.config["SMTP_PORT"])
            server.starttls()
            server.login(app.config["SMTP_USERNAME"], app.config["SMTP_PASSWORD"])

            # Send the email
            server.sendmail(app.config["SMTP_SENDER"], email, msg.as_string())
            server.quit()

            app.logger.info(f"Verification code sent to {email}")
            user_verify_code.verify_code_send = 1
            db.session.commit()
        except Exception as e:
            app.logger.error(f"Failed to send verification code to {email}: {str(e)}")
            raise_error("server.user.emailSendFailed")
        return {"expire_in": app.config["MAIL_CODE_EXPIRE_TIME"]}


def create_and_commit_user_verify_code(
    mail: str | None,
    phone: str | None,
    verify_code: str,
    verify_code_type: int,
    ip: str | None,
):
    user_verify_code = UserVerifyCode(
        phone=phone or "",
        mail=mail or "",
        verify_code=verify_code,
        verify_code_type=verify_code_type,  # 1: SMS, 2: Email
        verify_code_used=0,
        verify_code_send=0,
        user_ip=ip or "",
    )
    db.session.add(user_verify_code)
    db.session.commit()
    return user_verify_code


def ensure_creator_demo_permissions_and_first_lesson(
    app: Flask, user_id: str, language: str
) -> bool:
    """
    Ensure that a user is marked as creator and has demo course permissions.

    The function name is kept for compatibility. First lesson draft creation
    is handled by course creation flows.
    """
    creator_granted_now = mark_creator_role_if_needed(user_id)
    ensure_demo_course_permissions(app, user_id)
    return creator_granted_now


def load_existing_demo_shifu_ids() -> set[str]:
    configured_bids = {
        str(get_dynamic_config(key) or "").strip()
        for key in ("DEMO_SHIFU_BID", "DEMO_EN_SHIFU_BID")
    }
    configured_bids.discard("")
    if not configured_bids:
        return set()

    published_bids = {
        row[0]
        for row in PublishedShifu.query.filter(
            PublishedShifu.shifu_bid.in_(configured_bids),
            PublishedShifu.deleted == 0,
        )
        .with_entities(PublishedShifu.shifu_bid)
        .all()
        if row and row[0]
    }
    draft_bids = {
        row[0]
        for row in DraftShifu.query.filter(
            DraftShifu.shifu_bid.in_(configured_bids),
            DraftShifu.deleted == 0,
        )
        .with_entities(DraftShifu.shifu_bid)
        .all()
        if row and row[0]
    }
    return published_bids.union(draft_bids)


def _is_empty_auth_type(raw_auth_type) -> bool:
    text = str(raw_auth_type or "").strip()
    if not text:
        return True
    if text in {"[]", "null"}:
        return True
    try:
        parsed = json.loads(text)
    except (TypeError, json.JSONDecodeError):
        return False
    return parsed is None or (isinstance(parsed, list) and len(parsed) == 0)


def ensure_demo_course_permissions(
    app: Flask, user_id: str, demo_ids: set[str] | None = None
) -> None:
    """Grant configured demo course view permissions to a user."""
    effective_demo_ids = set(demo_ids or ()) if demo_ids is not None else None
    if effective_demo_ids is None:
        effective_demo_ids = load_existing_demo_shifu_ids()
    if not effective_demo_ids:
        return

    existing_auths = {
        auth.course_id: auth
        for auth in AiCourseAuth.query.filter(
            AiCourseAuth.user_id == user_id,
            AiCourseAuth.course_id.in_(effective_demo_ids),
        ).all()
    }
    view_auth_types = json.dumps(["view"])
    has_changes = False
    for shifu_bid in effective_demo_ids:
        auth = existing_auths.get(shifu_bid)
        if auth:
            if _is_empty_auth_type(auth.auth_type):
                auth.auth_type = view_auth_types
                has_changes = True
            if auth.status != 1:
                auth.status = 1
                has_changes = True
            continue

        db.session.add(
            AiCourseAuth(
                course_auth_id=generate_id(app),
                user_id=user_id,
                course_id=shifu_bid,
                auth_type=view_auth_types,
                status=1,
            )
        )
        has_changes = True
    if has_changes:
        db.session.flush()


def ensure_admin_creator_and_demo_permissions(
    app: Flask, user_id: str, language: str, login_context: str | None = None
) -> bool:
    """
    Ensure that an admin-login user is a creator and has demo course permissions.

    This helper is controlled by the ADMIN_LOGIN_GRANT_CREATOR_WITH_DEMO flag and
    is intended for demo/staging environments.
    """
    # Only apply when the feature flag is enabled
    if not app.config.get("ADMIN_LOGIN_GRANT_CREATOR_WITH_DEMO", False):
        return False

    # Only act on explicit admin logins
    if login_context != "admin":
        return False

    return ensure_creator_demo_permissions_and_first_lesson(app, user_id, language)
