import asyncio
import os
import secrets
import threading

from flask import Flask, jsonify, request
from telethon import TelegramClient, functions, types

# =========================================================
# Telegram configuration
# =========================================================

API_ID = int(os.getenv("TG_API_ID", "35010100"))

API_HASH = os.getenv(
    "TG_API_HASH",
    "501eda6dc49e8ed4d4d13942bfd3a73e"
)

SESSION = os.getenv(
    "TG_SESSION",
    "/home/mrgamers/public_html/sandgift/stars_account"
)

# Secret token shared between index.py and bot.py
API_TOKEN = os.getenv(
    "STARS_API_TOKEN",
    "9165c0f9589e8e1daf22f52b0f32a6a3fa94dab5a295823f9a9e15f53936caf"
)

# Internal API — keep it on localhost
HOST = os.getenv(
    "STARS_API_HOST",
    "127.0.0.1"
)

PORT = int(os.getenv(
    "STARS_API_PORT",
    "8765"
))


# =========================================================
# Flask
# =========================================================

app = Flask(__name__)


# =========================================================
# Telegram client
# =========================================================

client = TelegramClient(
    SESSION,
    API_ID,
    API_HASH
)

telegram_loop = asyncio.new_event_loop()

telegram_ready = threading.Event()
telegram_error = [None]


# =========================================================
# Start Telegram client in separate thread
# =========================================================

def telegram_thread():
    asyncio.set_event_loop(telegram_loop)

    async def start_client():
        await client.connect()

        authorized = await client.is_user_authorized()

        if not authorized:
            raise RuntimeError(
                "Telegram account is not authorized."
            )

        print("Telegram client connected successfully.")

    async def connection_watchdog():
        while True:
            try:
                if not client.is_connected():
                    print("Telegram disconnected. Reconnecting...")

                    try:
                        await client.connect()
                        print("Telegram reconnected successfully.")
                    except Exception as reconnect_error:
                        print(
                            f"Telegram reconnect failed: {reconnect_error}"
                        )

                await asyncio.sleep(10)

            except asyncio.CancelledError:
                raise

            except Exception as watchdog_error:
                print(
                    f"Telegram watchdog error: {watchdog_error}"
                )

                await asyncio.sleep(10)

    try:
        telegram_loop.run_until_complete(
            start_client()
        )

        telegram_ready.set()

        telegram_loop.create_task(
            connection_watchdog()
        )

        telegram_loop.run_forever()

    except Exception as exc:
        telegram_error[0] = str(exc)
        telegram_ready.set()


threading.Thread(
    target=telegram_thread,
    daemon=True
).start()


# Wait for Telegram client
telegram_ready.wait(180)


if telegram_error[0]:
    raise RuntimeError(
        telegram_error[0]
    )


# =========================================================
# Run coroutine inside Telegram event loop
# =========================================================

def run_async(coro, timeout=120):
    future = asyncio.run_coroutine_threadsafe(
        coro,
        telegram_loop
    )

    return future.result(
        timeout=timeout
    )


# =========================================================
# API authentication
# =========================================================

def check_api_token():
    token = request.headers.get(
        "X-API-Token",
        ""
    )

    if not token:
        return False

    return secrets.compare_digest(
        token,
        API_TOKEN
    )


@app.before_request
def protect_api():

    # Health endpoint is public
    if request.path == "/health":
        return None

    if not check_api_token():
        return jsonify({
            "ok": False,
            "error": "Unauthorized"
        }), 401


# =========================================================
# Helpers
# =========================================================

def get_amount(value):
    """
    Telegram Stars amount can contain amount/nanos.
    """

    amount = int(
        getattr(
            value,
            "amount",
            0
        )
    )

    nanos = int(
        getattr(
            value,
            "nanos",
            0
        )
    )

    return amount + (
        nanos / 1_000_000_000
    )


# =========================================================
# Get Stars balance
# =========================================================

async def telegram_balance():

    result = await client(
        functions.payments.GetStarsStatusRequest(
            peer=types.InputPeerSelf()
        )
    )

    return get_amount(
        result.balance
    )


# =========================================================
# Get Telegram Gifts
# =========================================================

async def telegram_gifts():

    result = await client(
        functions.payments.GetStarGiftsRequest(
            hash=0
        )
    )

    gifts = []

    for gift in result.gifts:

        gifts.append({
            "id": str(
                gift.id
            ),

            "stars": int(
                gift.stars
            ),

            "sold_out": bool(
                getattr(
                    gift,
                    "sold_out",
                    False
                )
            ),

            "require_premium": bool(
                getattr(
                    gift,
                    "require_premium",
                    False
                )
            ),

            "upgrade_stars": (
                getattr(
                    gift,
                    "upgrade_stars",
                    None
                )
            )
        })

    return gifts


# =========================================================
# Send Telegram Gift
# =========================================================

async def telegram_send_gift(
    username,
    gift_id,
    message="",
    hide_name=False,
    include_upgrade=False
):

    username = str(
        username
    ).strip().lstrip("@")

    if not username:
        raise ValueError(
            "Username is required."
        )

    gift_id = str(
        gift_id
    ).strip()

    if not gift_id:
        raise ValueError(
            "Gift ID is required."
        )

    # -----------------------------------------------------
    # Resolve recipient
    # -----------------------------------------------------

    try:
        peer = await client.get_input_entity(
            username
        )
    except Exception as exc:
        raise ValueError(
            f"Cannot find Telegram user: {exc}"
        )

    # -----------------------------------------------------
    # Load gifts
    # -----------------------------------------------------

    available_gifts = await telegram_gifts()

    gift = next(
        (
            item
            for item in available_gifts
            if item["id"] == gift_id
        ),
        None
    )

    if gift is None:
        raise ValueError(
            "Gift ID not found."
        )

    if gift["sold_out"]:
        raise ValueError(
            "This gift is sold out."
        )

    # -----------------------------------------------------
    # Upgrade check
    # -----------------------------------------------------

    if include_upgrade:

        if gift["upgrade_stars"] is None:
            raise ValueError(
                "This gift does not have a prepaid upgrade."
            )

    # -----------------------------------------------------
    # Check whether gift can be sent
    # -----------------------------------------------------

    try:

        await client(
            functions.payments.CheckCanSendGiftRequest(
                gift_id=int(gift_id),
                peer=peer
            )
        )

    except TypeError:

        # Compatibility with Telethon versions
        # where peer is not part of the request.
        try:

            await client(
                functions.payments.CheckCanSendGiftRequest(
                    gift_id=int(gift_id)
                )
            )

        except Exception as exc:
            raise ValueError(
                f"Telegram rejected this gift: {exc}"
            )

    except Exception as exc:

        raise ValueError(
            f"Telegram rejected this gift: {exc}"
        )

    # -----------------------------------------------------
    # Check Stars balance
    # -----------------------------------------------------

    balance_before = await telegram_balance()

    gift_price = int(
        gift["stars"]
    )

    upgrade_price = 0

    if include_upgrade:
        upgrade_price = int(
            gift["upgrade_stars"]
        )

    required_stars = (
        gift_price +
        upgrade_price
    )

    if balance_before < required_stars:

        raise ValueError(
            "Insufficient Stars: "
            f"{balance_before:g}/{required_stars}"
        )

    # -----------------------------------------------------
    # Prepare message
    # -----------------------------------------------------

    message = str(
        message or ""
    )[:255]

    invoice_message = types.TextWithEntities(
        text=message,
        entities=[]
    )

    # -----------------------------------------------------
    # Create Star Gift invoice
    # -----------------------------------------------------

    invoice = types.InputInvoiceStarGift(
        peer=peer,
        gift_id=int(gift_id),
        message=invoice_message,
        hide_name=bool(
            hide_name
        ),
        include_upgrade=bool(
            include_upgrade
        )
    )

    # -----------------------------------------------------
    # Get payment form
    # -----------------------------------------------------

    payment_form = await client(
        functions.payments.GetPaymentFormRequest(
            invoice=invoice
        )
    )

    # -----------------------------------------------------
    # Pay / send gift
    # -----------------------------------------------------

    result = await client(
        functions.payments.SendStarsFormRequest(
            form_id=payment_form.form_id,
            invoice=invoice
        )
    )

    # -----------------------------------------------------
    # Get new balance
    # -----------------------------------------------------

    balance_after = await telegram_balance()

    return {
        "result_type": type(
            result
        ).__name__,

        "recipient": "@" + username,

        "gift_id": str(
            gift_id
        ),

        "gift_stars": gift_price,

        "upgrade_stars": upgrade_price,

        "total_stars": required_stars,

        "balance_before": balance_before,

        "balance_after": balance_after
    }


# =========================================================
# Health
# =========================================================

@app.get("/health")
def health():

    try:

        authorized = run_async(
            client.is_user_authorized(),
            timeout=20
        )

        return jsonify({
            "ok": True,
            "telegram_authorized": bool(
                authorized
            )
        })

    except Exception as exc:

        return jsonify({
            "ok": False,
            "error": str(exc)
        }), 500


# =========================================================
# Balance API
# =========================================================

@app.get("/balance")
def balance_api():

    try:

        balance = run_async(
            telegram_balance(),
            timeout=30
        )

        return jsonify({
            "ok": True,
            "balance": balance
        })

    except Exception as exc:

        return jsonify({
            "ok": False,
            "error": str(exc)
        }), 500


# =========================================================
# Gifts API
# =========================================================

@app.get("/gifts")
def gifts_api():

    try:

        gifts = run_async(
            telegram_gifts(),
            timeout=30
        )

        return jsonify({
            "ok": True,
            "gifts": gifts
        })

    except Exception as exc:

        return jsonify({
            "ok": False,
            "error": str(exc)
        }), 500


# =========================================================
# Send Gift API
# =========================================================

@app.post("/send-gift")
def send_gift_api():

    data = request.get_json(
        silent=True
    ) or {}

    username = data.get(
        "username"
    )

    gift_id = data.get(
        "gift_id"
    )

    message = data.get(
        "message",
        ""
    )

    hide_name = bool(
        data.get(
            "hide_name",
            False
        )
    )

    include_upgrade = bool(
        data.get(
            "include_upgrade",
            False
        )
    )

    if not username:

        return jsonify({
            "ok": False,
            "error": "username is required"
        }), 400

    if not gift_id:

        return jsonify({
            "ok": False,
            "error": "gift_id is required"
        }), 400

    try:

        result = run_async(
            telegram_send_gift(
                username=username,
                gift_id=gift_id,
                message=message,
                hide_name=hide_name,
                include_upgrade=include_upgrade
            ),
            timeout=160
        )

        return jsonify({
            "ok": True,
            **result
        })

    except Exception as exc:

        return jsonify({
            "ok": False,
            "error": str(exc)
        }), 400


# =========================================================
# Main
# =========================================================

if __name__ == "__main__":

    print(
        "Telegram Stars API: "
        f"http://{HOST}:{PORT}"
    )

    app.run(
        host=HOST,
        port=PORT,
        debug=False,
        threaded=True
    )