import gzip
import hmac
import ipaddress
import os
import re
import shutil
import sqlite3
import tempfile
import threading
import time
from flask import Flask, request, jsonify, redirect

import config
import database as db
import telegram_api as tg

app = Flask(__name__)
db.init()

DEFAULT_PICK_NAME = "منتخب"


def cat_names():
    return {"default": "پیش‌فرض", "pick": db.get("cat_pick_name") or DEFAULT_PICK_NAME}


# ---------------------------------------------------------------------------
# امنیت و پایداری: محدودکردن درخواست‌ها + پشتیبان‌گیری روزانه‌ی دیتابیس
# ---------------------------------------------------------------------------

_hits = {}


def rate_ok(key, limit, window):
    """حداکثر limit درخواست در window ثانیه برای هر کلید (در حافظه‌ی همین پروسه)."""
    now = time.time()
    lst = [t for t in _hits.get(key, []) if now - t < window]
    if len(lst) >= limit:
        _hits[key] = lst
        return False
    lst.append(now)
    _hits[key] = lst
    if len(_hits) > 5000:  # پاکسازی کلیدهای قدیمی تا حافظه پر نشود
        for k in [k for k, v in _hits.items() if not v or now - v[-1] > 600]:
            _hits.pop(k, None)
    return True


def client_ip():
    return request.headers.get("X-Forwarded-For", "").split(",")[0].strip() or request.remote_addr or "?"


def make_backup():
    raw = os.path.join(tempfile.gettempdir(), "ping_backup.db")
    src, dst = sqlite3.connect(config.DB_PATH), sqlite3.connect(raw)
    src.backup(dst)
    dst.close()
    src.close()
    gz = raw + ".gz"
    with open(raw, "rb") as a, gzip.open(gz, "wb") as b:
        shutil.copyfileobj(a, b)
    os.remove(raw)
    return gz


def send_backup():
    """نسخه‌ی فشرده‌ی دیتابیس را برای اولین ادمین می‌فرستد."""
    gz = make_backup()
    try:
        ok = bool(tg.send_document(config.ADMIN_IDS[0], gz, "💾 پشتیبان دیتابیس Mors Ping").get("ok"))
    finally:
        os.remove(gz)
    if ok:
        db.put("last_backup", int(time.time()))
    return ok


_backup_lock = threading.Lock()


@app.before_request
def auto_backup():
    """هر ۲۴ ساعت یک‌بار (با اولین درخواست API بعد از آن) پشتیبان می‌فرستد؛ در صورت شکست، حداکثر هر ۳۰ دقیقه دوباره تلاش می‌کند."""
    if not request.path.startswith("/api/") or time.time() - int(db.get("last_backup", "0")) < 86400:
        return
    if time.time() - int(db.get("last_backup_try", "0")) < 1800 or not _backup_lock.acquire(blocking=False):
        return
    try:
        db.put("last_backup_try", int(time.time()))
        send_backup()
    except Exception:
        app.logger.exception("backup failed")
    finally:
        _backup_lock.release()


# ---------------------------------------------------------------------------
# API عمومی برای اپ
# ---------------------------------------------------------------------------

@app.route("/api/dns")
def api_dns():
    return jsonify([{"id": r["id"], "name": r["name"], "ip1": r["ip1"], "ip2": r["ip2"], "cat": r["cat"]} for r in db.list_dns()])


@app.route("/api/ads")
def api_ads():
    return jsonify([{"id": r["id"], "title": r["title"], "image": r["image_url"], "click": f"/ad/{r['id']}"} for r in db.list_ads()])


@app.route("/api/app")
def api_app():
    """تنظیمات عمومی اپ: لینک ربات فروشگاه، نام دسته‌ها، اعلان داخل اپ و اطلاعات نسخه."""
    notice = None
    if db.get("notice_text"):
        notice = {"id": int(db.get("notice_id", "0")), "text": db.get("notice_text"), "link": db.get("notice_link") or None}
    return jsonify({
        "bot_url": db.get("shop_url", ""),
        "categories": cat_names(),
        "notice": notice,
        "version": {"latest": db.get("ver_latest"), "min": db.get("ver_min"), "url": db.get("ver_url"), "text": db.get("ver_text")},
    })


@app.route("/api/stat", methods=["POST"])
def api_stat():
    """رویداد ناشناس اپ: شناسه‌ی تصادفی دستگاه + نسخه + (در صورت اعمال DNS) شماره‌ی آن."""
    j = request.get_json(silent=True) or {}
    device = str(j.get("device", ""))
    if not (8 <= len(device) <= 64) or not device.isalnum():
        return jsonify({"ok": False}), 400
    if not rate_ok(("ip", client_ip()), 60, 60) or not rate_ok(("dev", device), 20, 60):
        return jsonify({"ok": False}), 429
    ver = str(j.get("version", ""))
    db.touch_device(device, ver if re.fullmatch(r"\d+(\.\d+){0,3}", ver) else None)
    if j.get("event") == "apply" and isinstance(j.get("dns_id"), int) and j["dns_id"] > 0:
        db.log_apply(j["dns_id"], device)
    return jsonify({"ok": True})


@app.route("/ad/<int:ad_id>")
def ad_click(ad_id):
    link = db.click_ad(ad_id)
    return redirect(link) if link else ("", 404)


# ---------------------------------------------------------------------------
# پنل ادمین (داخل ربات)
# ---------------------------------------------------------------------------

def kb(*rows):
    return {"inline_keyboard": [[{"text": t, "callback_data": d} for t, d in row] for row in rows]}


def show(chat_id, mid, text, markup):
    if mid:
        r = tg.edit_message_text(chat_id, mid, text, markup)
        if r.get("ok") or "not modified" in str(r.get("description", "")):
            return
    tg.send_message(chat_id, text, markup)


BACK = [("بازگشت به منو", "m")]


def main_menu(chat_id, mid=None):
    n = cat_names()
    show(chat_id, mid, "📡 مدیریت اپ Mors Ping",
         kb([(f"➕ DNS به «{n['pick']}»", "dns_add:pick")], [(f"➕ DNS به «{n['default']}»", "dns_add:default")],
            [("🗑 حذف DNS", "dns_list")], [("📢 تبلیغات", "ads"), ("📰 اعلان داخل اپ", "notice")],
            [("🔢 نسخه‌ی اپ", "ver"), ("📊 آمار", "stats")], [("⚙️ تنظیمات", "cfg")]))


VER_KEYS = {"ver_latest": ("آخرین نسخه (مثلاً 1.2.0)", True), "ver_min": ("حداقل نسخه‌ی مجاز (مثلاً 1.0.0)", True),
            "ver_url": ("لینک دانلود نسخه‌ی جدید (https)", False), "ver_text": ("متن پیام به‌روزرسانی", False),
            "cat_pick_name": ("نام دسته‌ی دوم در اپ", False), "shop_url": ("لینک ربات فروشگاه (https://t.me/...)", False)}


def valid_ip(s):
    try:
        ipaddress.IPv4Address(s)
        return True
    except ValueError:
        return False


def screen(chat_id, mid, data):
    if data == "m":
        return main_menu(chat_id, mid)
    if data.startswith("dns_add:"):
        cat = data.split(":")[1]
        db.set_state(chat_id, "dns", {"cat": cat})
        return show(chat_id, mid, f"➕ افزودن به «{cat_names()[cat]}»\nنام | آی‌پی اول | آی‌پی دوم (اختیاری)\nمثال: کلودفلر | 1.1.1.1 | 1.0.0.1\n\n(/cancel برای انصراف)", kb(BACK))
    if data == "dns_list":
        nm = cat_names()
        rows = [[(f"🗑 {r['name']} ({r['ip1']}) • {nm[r['cat']]}", f"dns_del:{r['id']}")] for r in db.list_dns()]
        return show(chat_id, mid, "برای حذف روی DNS بزنید:" if rows else "DNS ای ثبت نشده.", kb(*rows, BACK))
    if data == "ads":
        rows = [[(f"🗑 {r['title']} • {r['clicks']} کلیک", f"ad_del:{r['id']}")] for r in db.list_ads()]
        return show(chat_id, mid, "📢 تبلیغات (برای حذف روی هرکدام بزنید)", kb([("➕ افزودن تبلیغ", "ad_add")], *rows, BACK))
    if data == "ad_add":
        db.set_state(chat_id, "ad")
        return show(chat_id, mid, "عنوان | آدرس تصویر (https یا -) | لینک مقصد\nمثال: تخفیف ویژه | https://site.com/b.png | https://t.me/mors\n\n(/cancel برای انصراف)", kb(BACK))
    if data == "notice":
        cur = db.get("notice_text")
        return show(chat_id, mid, "📰 اعلان داخل اپ (بالای صفحه‌ی خانه نشان داده می‌شود)\n\nاعلان فعلی: " + (cur or "ندارد"),
                    kb([("✏️ تنظیم اعلان", "notice_set")], [("❌ حذف اعلان", "notice_off")], BACK))
    if data == "notice_set":
        db.set_state(chat_id, "notice")
        return show(chat_id, mid, "متن اعلان | لینک (اختیاری)\nمثال: سرور جدید اضافه شد | https://t.me/mors\n\n(/cancel برای انصراف)", kb(BACK))
    if data == "notice_off":
        db.put("notice_text", "")
        return screen(chat_id, mid, "notice")
    if data == "ver":
        g = lambda k: db.get(k) or "—"
        return show(chat_id, mid, f"🔢 نسخه‌ی اپ\n\nآخرین نسخه: {g('ver_latest')}\nحداقل نسخه: {g('ver_min')}\nلینک دانلود: {g('ver_url')}\nمتن: {g('ver_text')}\n\n"
                    "• کمتر از «آخرین»: به کاربر پیشنهاد به‌روزرسانی داده می‌شود.\n• کمتر از «حداقل»: اپ تا به‌روزرسانی قفل می‌شود.",
                    kb([("آخرین نسخه", "set:ver_latest"), ("حداقل نسخه", "set:ver_min")], [("لینک دانلود", "set:ver_url"), ("متن پیام", "set:ver_text")], BACK))
    if data == "cfg":
        lb = int(db.get("last_backup", "0"))
        return show(chat_id, mid, f"⚙️ تنظیمات\n\nنام دسته‌ی دوم: {cat_names()['pick']}\nربات فروشگاه: {db.get('shop_url') or '—'}\n"
                    f"آخرین پشتیبان: {time.strftime('%Y-%m-%d %H:%M', time.localtime(lb)) if lb else 'هنوز نشده'} (خودکار هر ۲۴ ساعت)",
                    kb([("نام دسته‌ی دوم", "set:cat_pick_name")], [("لینک ربات فروشگاه", "set:shop_url")], [("💾 پشتیبان‌گیری همین الان", "backup")], BACK))
    if data == "backup":
        ok = send_backup()
        return show(chat_id, mid, "✅ پشتیبان برایتان فرستاده شد." if ok else "⚠️ ارسال پشتیبان ناموفق بود.", kb([("بازگشت به تنظیمات", "cfg")], BACK))
    if data.startswith("set:"):
        key = data.split(":")[1]
        db.set_state(chat_id, "set", {"key": key})
        return show(chat_id, mid, f"{VER_KEYS[key][0]} را بفرستید.\n\n(/cancel برای انصراف)", kb(BACK))
    if data == "stats":
        s = db.stats()
        t = f"📊 آمار اپ\nکل دستگاه‌ها: {s['total']}\nفعال ۲۴ ساعت: {s['d1']}\nفعال ۷ روز: {s['d7']}\n\nنسخه‌ها (۳۰ روز اخیر):\n"
        t += "\n".join(f"{v['v']}: {v['n']} دستگاه" for v in s["versions"]) or "—"
        t += "\n\nمحبوبیت DNS ها (۷ روز اخیر):\n" + ("\n".join(f"{d['name']}: {d['devs']} دستگاه ({d['n']} بار)" for d in s["per_dns"]) or "—")
        return show(chat_id, mid, t, kb(BACK))


def handle_text(chat_id, text):
    st = db.get_state(chat_id)
    if text == "/cancel":
        db.clear_state(chat_id)
        return main_menu(chat_id)
    if not st:
        return main_menu(chat_id)
    parts = [p.strip() for p in text.split("|")]
    arg = lambda i: parts[i] if i < len(parts) and parts[i] not in ("", "-") else None
    act, data = st["action"], st["data"]
    if act == "dns":
        if not arg(0) or not arg(1) or not valid_ip(arg(1)) or (arg(2) and not valid_ip(arg(2))):
            return tg.send_message(chat_id, "فرمت یا آی‌پی نامعتبر است. مثال: کلودفلر | 1.1.1.1 | 1.0.0.1")
        db.add_dns(arg(0), arg(1), arg(2), data["cat"])
        db.clear_state(chat_id)
        return tg.send_message(chat_id, f"✅ به «{cat_names()[data['cat']]}» اضافه شد (همان لحظه در اپ دیده می‌شود).", kb(BACK))
    if act == "ad":
        if not arg(0) or not arg(2) or not arg(2).startswith(("http://", "https://", "tg://")) or (arg(1) and not arg(1).startswith("https://")):
            return tg.send_message(chat_id, "فرمت نامعتبر. مثال: تخفیف | https://site.com/b.png | https://t.me/mors")
        db.add_ad(arg(0), arg(1), arg(2))
        db.clear_state(chat_id)
        return tg.send_message(chat_id, "✅ تبلیغ اضافه شد.", kb(BACK))
    if act == "notice":
        if not arg(0):
            return tg.send_message(chat_id, "متن اعلان را بنویسید.")
        db.put("notice_text", arg(0)[:300])
        db.put("notice_link", arg(1) or "")
        db.put("notice_id", int(time.time()))  # با هر اعلان تازه، دکمه‌ی «بستن» کاربران دوباره برای اعلان جدید فعال می‌شود
        db.clear_state(chat_id)
        return tg.send_message(chat_id, "✅ اعلان فعال شد.", kb(BACK))
    if act == "set":
        key, val = data["key"], text.strip()
        if VER_KEYS[key][1] and not re.fullmatch(r"\d+(\.\d+){0,3}", val):
            return tg.send_message(chat_id, "نسخه را به شکل 1.2.0 بفرستید.")
        if key in ("ver_url", "shop_url") and not val.startswith("https://"):
            return tg.send_message(chat_id, "لینک باید با https:// شروع شود.")
        db.put(key, val[:200])
        db.clear_state(chat_id)
        return tg.send_message(chat_id, "✅ ذخیره شد.", kb(BACK))


@app.route("/webhook", methods=["POST"])
def webhook():
    if not hmac.compare_digest(request.headers.get("X-Telegram-Bot-Api-Secret-Token", ""), config.WEBHOOK_SECRET):
        return "forbidden", 403
    u = request.get_json(silent=True) or {}
    try:
        if "callback_query" in u:
            q = u["callback_query"]
            chat_id, mid = q["from"]["id"], q.get("message", {}).get("message_id")
            if chat_id not in config.ADMIN_IDS:
                tg.answer_callback_query(q["id"], "دسترسی ندارید.", True)
                return "ok"
            tg.answer_callback_query(q["id"])
            if q["data"].startswith("dns_del:") or q["data"].startswith("ad_del:"):
                kind, i = q["data"].split(":")
                (db.del_dns if kind == "dns_del" else db.del_ad)(int(i))
                return screen(chat_id, mid, "dns_list" if kind == "dns_del" else "ads") or "ok"
            if q["data"] not in ("m", "dns_list", "ads", "ad_add", "notice", "notice_set", "notice_off", "ver", "cfg", "stats", "backup") \
                    and not q["data"].startswith(("dns_add:", "set:")):
                return "ok"
            screen(chat_id, mid, q["data"])
        elif "message" in u and "text" in u["message"]:
            chat_id = u["message"]["chat"]["id"]
            if chat_id in config.ADMIN_IDS:
                handle_text(chat_id, u["message"]["text"])
            else:
                tg.send_message(chat_id, "این ربات فقط برای مدیریت اپ Mors Ping است.")
    except Exception:
        app.logger.exception("webhook error")
    return "ok"


@app.route("/set_webhook")
def set_webhook():
    if not hmac.compare_digest(request.args.get("secret", ""), config.WEBHOOK_SECRET):
        return "forbidden", 403
    return jsonify(tg.set_webhook(config.BASE_URL + "/webhook", config.WEBHOOK_SECRET))


@app.route("/")
def index():
    return "Mors Ping API"
