import os
import re
import time
import logging
from typing import Any, Dict, Optional, Tuple

import requests
from flask import Flask, request, jsonify


app = Flask(__name__)

# WSGI entry point for cPanel
application = app


# ============================================================
# Fixed Settings
# ============================================================

CHANNEL_USERNAME = "@Bikalammusicworld"

MAX_USER_MESSAGE_LENGTH = 1000
MAX_ADMIN_REPLY_LENGTH = 1000

RATE_LIMIT_WINDOW_SECONDS = 60
MAX_MESSAGES_PER_WINDOW = 5

TELEGRAM_TIMEOUT_SECONDS = 10


# ============================================================
# Environment Variables
# Only these three environment variables are allowed:
# BOT_TOKEN
# ADMIN_1_ID
# ADMIN_2_ID
# ============================================================

BOT_TOKEN = os.getenv("BOT_TOKEN")
ADMIN_1_ID = os.getenv("ADMIN_1_ID")
ADMIN_2_ID = os.getenv("ADMIN_2_ID")


# ============================================================
# Secure Logging
# ============================================================

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)

logger = logging.getLogger(__name__)


# ============================================================
# Temporary In-Memory Storage
# ============================================================

user_states: Dict[int, str] = {}
rate_limit_storage: Dict[int, list] = {}
processed_updates: Dict[int, float] = {}


# ============================================================
# Configuration Validation
# ============================================================

CONFIG_ERRORS = []
ADMIN_IDS = set()

if not BOT_TOKEN:
    CONFIG_ERRORS.append("BOT_TOKEN is missing.")

for env_name, env_value in {
    "ADMIN_1_ID": ADMIN_1_ID,
    "ADMIN_2_ID": ADMIN_2_ID
}.items():
    if not env_value:
        CONFIG_ERRORS.append(f"{env_name} is missing.")
    else:
        try:
            ADMIN_IDS.add(int(env_value))
        except ValueError:
            CONFIG_ERRORS.append(f"{env_name} must be a numeric Telegram user ID.")

CONFIG_IS_VALID = len(CONFIG_ERRORS) == 0

TELEGRAM_API_BASE = f"https://api.telegram.org/bot{BOT_TOKEN}" if BOT_TOKEN else ""


# ============================================================
# Telegram API Helpers
# ============================================================

def call_telegram_api(method: str, payload: Dict[str, Any]) -> Dict[str, Any]:
    if not CONFIG_IS_VALID:
        logger.error("Telegram API call skipped because configuration is invalid.")
        return {
            "ok": False,
            "description": "Bot configuration is incomplete."
        }

    url = f"{TELEGRAM_API_BASE}/{method}"

    try:
        response = requests.post(
            url,
            json=payload,
            timeout=TELEGRAM_TIMEOUT_SECONDS
        )

        try:
            data = response.json()
        except ValueError:
            logger.error("Telegram API returned non-JSON response for method: %s", method)
            return {
                "ok": False,
                "description": "Invalid response from Telegram API."
            }

        if not data.get("ok"):
            logger.warning(
                "Telegram API method %s failed. Error code: %s, Description: %s",
                method,
                data.get("error_code"),
                data.get("description")
            )

        return data

    except requests.RequestException as exc:
        logger.error("Telegram API request failed for method %s: %s", method, str(exc))
        return {
            "ok": False,
            "description": "Telegram API request failed."
        }


def send_message(
    chat_id: int,
    text: str,
    reply_markup: Optional[Dict[str, Any]] = None,
    reply_to_message_id: Optional[int] = None
) -> Dict[str, Any]:
    payload: Dict[str, Any] = {
        "chat_id": chat_id,
        "text": text,
        "disable_web_page_preview": True
    }

    if reply_markup:
        payload["reply_markup"] = reply_markup

    if reply_to_message_id:
        payload["reply_to_message_id"] = reply_to_message_id

    return call_telegram_api("sendMessage", payload)


def answer_callback_query(callback_query_id: str, text: str = "") -> None:
    payload = {
        "callback_query_id": callback_query_id
    }

    if text:
        payload["text"] = text

    call_telegram_api("answerCallbackQuery", payload)


# ============================================================
# Keyboards
# ============================================================

def main_menu_keyboard() -> Dict[str, Any]:
    return {
        "inline_keyboard": [
            [
                {
                    "text": "ارسال پیشنهاد یا انتقاد",
                    "callback_data": "feedback"
                }
            ],
            [
                {
                    "text": "درخواست موزیک",
                    "callback_data": "music_request"
                }
            ],
            [
                {
                    "text": "راهنما",
                    "callback_data": "help"
                }
            ]
        ]
    }


def force_join_keyboard() -> Dict[str, Any]:
    channel_url = f"https://t.me/{CHANNEL_USERNAME.replace('@', '')}"

    return {
        "inline_keyboard": [
            [
                {
                    "text": "عضویت در کانال",
                    "url": channel_url
                }
            ],
            [
                {
                    "text": "بررسی عضویت",
                    "callback_data": "check_membership"
                }
            ]
        ]
    }


# ============================================================
# User / Admin Helpers
# ============================================================

def is_admin(user_id: int) -> bool:
    return user_id in ADMIN_IDS


def get_user_display_name(user: Dict[str, Any]) -> str:
    first_name = user.get("first_name") or ""
    last_name = user.get("last_name") or ""
    full_name = f"{first_name} {last_name}".strip()
    return full_name if full_name else "بدون نام"


def get_username(user: Dict[str, Any]) -> str:
    username = user.get("username")
    return f"@{username}" if username else "ندارد"


def extract_message_text(message: Dict[str, Any]) -> str:
    return (message.get("text") or message.get("caption") or "").strip()


# ============================================================
# Membership Check
# ============================================================

def check_user_membership(user_id: int) -> Tuple[bool, str]:
    payload = {
        "chat_id": CHANNEL_USERNAME,
        "user_id": user_id
    }

    data = call_telegram_api("getChatMember", payload)

    if not data.get("ok"):
        return False, "membership_check_failed"

    result = data.get("result", {})
    status = result.get("status")

    if status in {"creator", "administrator", "member"}:
        return True, "member"

    if status == "restricted" and result.get("is_member") is True:
        return True, "member"

    return False, "not_member"


def send_force_join_message(chat_id: int) -> None:
    text = (
        "برای استفاده از امکانات ربات، ابتدا باید عضو کانال موسیقی بی‌کلام شوید.\n\n"
        f"کانال: {CHANNEL_USERNAME}\n\n"
        "پس از عضویت، روی دکمه «بررسی عضویت» بزنید."
    )

    send_message(
        chat_id=chat_id,
        text=text,
        reply_markup=force_join_keyboard()
    )


# ============================================================
# Rate Limit
# ============================================================

def is_rate_limited(user_id: int) -> bool:
    now = time.time()
    timestamps = rate_limit_storage.get(user_id, [])

    recent_timestamps = [
        ts for ts in timestamps
        if now - ts < RATE_LIMIT_WINDOW_SECONDS
    ]

    if len(recent_timestamps) >= MAX_MESSAGES_PER_WINDOW:
        rate_limit_storage[user_id] = recent_timestamps
        return True

    recent_timestamps.append(now)
    rate_limit_storage[user_id] = recent_timestamps
    return False


def cleanup_old_runtime_data() -> None:
    now = time.time()

    old_update_ids = [
        update_id for update_id, ts in processed_updates.items()
        if now - ts > 3600
    ]

    for update_id in old_update_ids:
        processed_updates.pop(update_id, None)


# ============================================================
# Admin Reports
# ============================================================

def build_admin_report(
    user: Dict[str, Any],
    request_type: str,
    message_text: str
) -> str:
    user_id = user.get("id")
    full_name = get_user_display_name(user)
    username = get_username(user)

    if request_type == "feedback":
        readable_type = "پیشنهاد / انتقاد"
        title = "پیام جدید از بخش پیشنهادات و انتقادات"
    else:
        readable_type = "درخواست موزیک"
        title = "درخواست موزیک برای کانال موسیقی بی‌کلام @Bikalammusicworld"

    report = (
        f"{title}\n"
        "-------------------------\n"
        f"نام کاربر: {full_name}\n"
        f"نام کاربری: {username}\n"
        f"آیدی عددی کاربر: {user_id}\n"
        f"USER_ID: {user_id}\n"
        f"نوع پیام: {readable_type}\n"
        "-------------------------\n"
        "متن پیام:\n"
        f"{message_text}\n"
        "-------------------------\n"
        "برای پاسخ سریع، روی همین پیام Reply کنید و متن پاسخ را بنویسید.\n"
        "یا از دستور زیر استفاده کنید:\n"
        f"/reply {user_id} متن پاسخ"
    )

    return report


def send_report_to_admins(
    user: Dict[str, Any],
    request_type: str,
    message_text: str
) -> bool:
    report_text = build_admin_report(user, request_type, message_text)

    success_count = 0

    for admin_id in ADMIN_IDS:
        result = send_message(
            chat_id=admin_id,
            text=report_text
        )

        if result.get("ok"):
            success_count += 1

    return success_count > 0


# ============================================================
# Admin Reply Handling
# ============================================================

def extract_user_id_from_replied_message(reply_to_message: Dict[str, Any]) -> Optional[int]:
    original_text = (
        reply_to_message.get("text")
        or reply_to_message.get("caption")
        or ""
    )

    match = re.search(r"USER_ID:\s*(\d+)", original_text)

    if not match:
        return None

    try:
        return int(match.group(1))
    except ValueError:
        return None


def send_admin_reply_to_user(
    admin_chat_id: int,
    target_user_id: int,
    reply_text: str
) -> None:
    if len(reply_text) > MAX_ADMIN_REPLY_LENGTH:
        send_message(
            chat_id=admin_chat_id,
            text=f"متن پاسخ بیش از حد مجاز است. حداکثر طول پاسخ {MAX_ADMIN_REPLY_LENGTH} کاراکتر است."
        )
        return

    message_to_user = (
        "پاسخ ادمین کانال موسیقی بی‌کلام:\n\n"
        f"{reply_text}"
    )

    result = send_message(
        chat_id=target_user_id,
        text=message_to_user
    )

    if result.get("ok"):
        send_message(
            chat_id=admin_chat_id,
            text="پاسخ با موفقیت برای کاربر ارسال شد."
        )
    else:
        send_message(
            chat_id=admin_chat_id,
            text=(
                "ارسال پاسخ به کاربر انجام نشد.\n"
                "ممکن است کاربر ربات را مسدود کرده باشد، چت را شروع نکرده باشد، "
                "یا Telegram API خطا داده باشد."
            )
        )


def handle_admin_direct_reply(message: Dict[str, Any]) -> bool:
    user = message.get("from", {})
    admin_user_id = user.get("id")
    chat_id = message.get("chat", {}).get("id")

    if not admin_user_id or not is_admin(admin_user_id):
        return False

    reply_to_message = message.get("reply_to_message")

    if not reply_to_message:
        return False

    target_user_id = extract_user_id_from_replied_message(reply_to_message)

    if not target_user_id:
        return False

    reply_text = extract_message_text(message)

    if not reply_text:
        send_message(
            chat_id=chat_id,
            text="متن پاسخ خالی است. لطفاً یک پیام متنی برای کاربر ارسال کنید."
        )
        return True

    send_admin_reply_to_user(
        admin_chat_id=chat_id,
        target_user_id=target_user_id,
        reply_text=reply_text
    )

    return True


def handle_admin_reply_command(message: Dict[str, Any]) -> bool:
    user = message.get("from", {})
    admin_user_id = user.get("id")
    chat_id = message.get("chat", {}).get("id")
    text = extract_message_text(message)

    if not text.startswith("/reply"):
        return False

    if not admin_user_id or not is_admin(admin_user_id):
        send_message(
            chat_id=chat_id,
            text="شما اجازه استفاده از این دستور را ندارید."
        )
        return True

    parts = text.split(maxsplit=2)

    if len(parts) < 3:
        send_message(
            chat_id=chat_id,
            text=(
                "فرمت دستور صحیح نیست.\n\n"
                "فرمت درست:\n"
                "/reply USER_ID متن پیام\n\n"
                "مثال:\n"
                "/reply 123456789 سلام، درخواست شما دریافت شد."
            )
        )
        return True

    _, user_id_text, reply_text = parts

    if not user_id_text.isdigit():
        send_message(
            chat_id=chat_id,
            text="USER_ID باید فقط عدد باشد."
        )
        return True

    target_user_id = int(user_id_text)
    reply_text = reply_text.strip()

    if not reply_text:
        send_message(
            chat_id=chat_id,
            text="متن پاسخ نمی‌تواند خالی باشد."
        )
        return True

    send_admin_reply_to_user(
        admin_chat_id=chat_id,
        target_user_id=target_user_id,
        reply_text=reply_text
    )

    return True


# ============================================================
# User Flow
# ============================================================

def send_start_menu(chat_id: int) -> None:
    text = (
        "سلام و خوش آمدید.\n\n"
        "به ربات رسمی کانال موسیقی بی‌کلام خوش آمدید.\n"
        f"کانال: {CHANNEL_USERNAME}\n\n"
        "لطفاً یکی از گزینه‌های زیر را انتخاب کنید:"
    )

    send_message(
        chat_id=chat_id,
        text=text,
        reply_markup=main_menu_keyboard()
    )


def send_help(chat_id: int) -> None:
    text = (
        "راهنمای استفاده از ربات:\n\n"
        "۱. برای ارسال نظر، پیشنهاد یا انتقاد، گزینه «ارسال پیشنهاد یا انتقاد» را انتخاب کنید.\n"
        "۲. برای درخواست موسیقی، گزینه «درخواست موزیک» را انتخاب کنید.\n"
        "۳. پیام شما برای ادمین‌های کانال ارسال می‌شود.\n"
        "۴. در صورت نیاز، ادمین از طریق همین ربات به شما پاسخ خواهد داد.\n\n"
        f"کانال موسیقی بی‌کلام: {CHANNEL_USERNAME}"
    )

    send_message(chat_id=chat_id, text=text)


def handle_start_command(message: Dict[str, Any]) -> None:
    user = message.get("from", {})
    user_id = user.get("id")
    chat_id = message.get("chat", {}).get("id")

    if not user_id or not chat_id:
        return

    is_member, _reason = check_user_membership(user_id)

    if not is_member:
        send_force_join_message(chat_id)
        return

    send_start_menu(chat_id)


def handle_user_content_message(message: Dict[str, Any]) -> None:
    user = message.get("from", {})
    user_id = user.get("id")
    chat_id = message.get("chat", {}).get("id")

    if not user_id or not chat_id:
        return

    is_member, _reason = check_user_membership(user_id)

    if not is_member:
        send_force_join_message(chat_id)
        return

    if is_rate_limited(user_id):
        send_message(
            chat_id=chat_id,
            text=(
                "تعداد پیام‌های شما در مدت کوتاه زیاد بوده است.\n"
                "لطفاً کمی بعد دوباره تلاش کنید."
            )
        )
        return

    current_state = user_states.get(user_id)

    if not current_state:
        send_message(
            chat_id=chat_id,
            text="لطفاً ابتدا یکی از گزینه‌های منو را انتخاب کنید.",
            reply_markup=main_menu_keyboard()
        )
        return

    message_text = extract_message_text(message)

    if not message_text:
        send_message(
            chat_id=chat_id,
            text="لطفاً پیام خود را به‌صورت متن ارسال کنید."
        )
        return

    if len(message_text) > MAX_USER_MESSAGE_LENGTH:
        send_message(
            chat_id=chat_id,
            text=f"متن پیام بیش از حد مجاز است. حداکثر طول پیام {MAX_USER_MESSAGE_LENGTH} کاراکتر است."
        )
        return

    if current_state == "feedback":
        success = send_report_to_admins(user, "feedback", message_text)

        if success:
            send_message(
                chat_id=chat_id,
                text="پیام شما با موفقیت ثبت شد و برای ادمین‌های کانال ارسال گردید."
            )
            user_states.pop(user_id, None)
        else:
            send_message(
                chat_id=chat_id,
                text="در حال حاضر ارسال پیام به ادمین‌ها انجام نشد. لطفاً بعداً دوباره تلاش کنید."
            )

    elif current_state == "music_request":
        success = send_report_to_admins(user, "music_request", message_text)

        if success:
            send_message(
                chat_id=chat_id,
                text="درخواست موزیک شما با موفقیت ثبت شد و برای ادمین‌های کانال ارسال گردید."
            )
            user_states.pop(user_id, None)
        else:
            send_message(
                chat_id=chat_id,
                text="در حال حاضر ارسال درخواست به ادمین‌ها انجام نشد. لطفاً بعداً دوباره تلاش کنید."
            )

    else:
        user_states.pop(user_id, None)
        send_message(
            chat_id=chat_id,
            text="وضعیت قبلی نامعتبر بود. لطفاً دوباره از منو گزینه موردنظر را انتخاب کنید.",
            reply_markup=main_menu_keyboard()
        )


# ============================================================
# Callback Query Handling
# ============================================================

def handle_callback_query(callback_query: Dict[str, Any]) -> None:
    callback_query_id = callback_query.get("id")
    data = callback_query.get("data")
    user = callback_query.get("from", {})
    user_id = user.get("id")

    message = callback_query.get("message", {})
    chat_id = message.get("chat", {}).get("id")

    if callback_query_id:
        answer_callback_query(callback_query_id)

    if not user_id or not chat_id:
        return

    if data == "check_membership":
        is_member, reason = check_user_membership(user_id)

        if is_member:
            send_message(
                chat_id=chat_id,
                text="عضویت شما تأیید شد."
            )
            send_start_menu(chat_id)
        else:
            if reason == "membership_check_failed":
                send_message(
                    chat_id=chat_id,
                    text=(
                        "امکان بررسی عضویت در حال حاضر فراهم نشد.\n"
                        "لطفاً مطمئن شوید عضو کانال شده‌اید و کمی بعد دوباره تلاش کنید."
                    ),
                    reply_markup=force_join_keyboard()
                )
            else:
                send_force_join_message(chat_id)

        return

    is_member, _reason = check_user_membership(user_id)

    if not is_member:
        send_force_join_message(chat_id)
        return

    if data == "feedback":
        user_states[user_id] = "feedback"
        send_message(
            chat_id=chat_id,
            text=(
                "لطفاً متن پیشنهاد، انتقاد یا نظر خود را ارسال کنید.\n\n"
                f"حداکثر طول پیام: {MAX_USER_MESSAGE_LENGTH} کاراکتر"
            )
        )
        return

    if data == "music_request":
        user_states[user_id] = "music_request"
        send_message(
            chat_id=chat_id,
            text=(
                "لطفاً نام موسیقی، نام هنرمند، سبک یا توضیح موردنظر خود را ارسال کنید.\n\n"
                f"حداکثر طول پیام: {MAX_USER_MESSAGE_LENGTH} کاراکتر"
            )
        )
        return

    if data == "help":
        send_help(chat_id)
        return

    send_message(
        chat_id=chat_id,
        text="گزینه انتخاب‌شده معتبر نیست. لطفاً دوباره از منو انتخاب کنید.",
        reply_markup=main_menu_keyboard()
    )


# ============================================================
# Update Dispatcher
# ============================================================

def is_duplicate_update(update_id: int) -> bool:
    if update_id in processed_updates:
        return True

    processed_updates[update_id] = time.time()
    return False


def process_update(update: Dict[str, Any]) -> None:
    cleanup_old_runtime_data()

    update_id = update.get("update_id")

    if isinstance(update_id, int) and is_duplicate_update(update_id):
        return

    if "callback_query" in update:
        handle_callback_query(update["callback_query"])
        return

    message = update.get("message") or update.get("edited_message")

    if not message:
        return

    if handle_admin_direct_reply(message):
        return

    if handle_admin_reply_command(message):
        return

    text = extract_message_text(message)

    if text == "/start" or text.startswith("/start "):
        handle_start_command(message)
        return

    handle_user_content_message(message)


# ============================================================
# Flask Routes
# ============================================================

@app.route("/", methods=["GET"])
def index():
    if CONFIG_IS_VALID:
        return "Bot is running", 200

    return (
        "Bot is running, but configuration is incomplete. "
        "Please check BOT_TOKEN, ADMIN_1_ID, ADMIN_2_ID.",
        200
    )


@app.route("/webhook", methods=["POST"])
def webhook():
    if not CONFIG_IS_VALID:
        logger.error("Webhook received but configuration is invalid: %s", CONFIG_ERRORS)
        return jsonify({"ok": False, "error": "Configuration is incomplete."}), 200

    if not request.is_json:
        return jsonify({"ok": False, "error": "Invalid content type."}), 400

    try:
        update = request.get_json(silent=True)

        if not isinstance(update, dict):
            return jsonify({"ok": False, "error": "Invalid update."}), 400

        process_update(update)

    except Exception as exc:
        logger.exception("Unhandled error while processing update: %s", str(exc))
        return jsonify({"ok": False}), 200

    return jsonify({"ok": True}), 200


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)