import json, os, urllib.request, urllib.error, asyncio, time, hashlib, secrets, re
from aiohttp import web
import aiomysql

DB_CONFIG = {
    "host": "localhost",
    "port": 3306,
    "user": "phonesender",
    "password": "phonesender_pass",
    "db": "phonesender",
    "autocommit": True
}

API_BASE = "https://api.sms24h.org/stubs/handler_api"
COUNTRY_BR = "73"
SERVICES_NOME = {"ev": "PicPay", "btn": "Itau"}

pool = None
# user_id -> set of websockets
user_phones = {}

# ---- Helpers ----

def hash_password(pw):
    return hashlib.sha256(pw.encode()).hexdigest()

def gen_token():
    return secrets.token_hex(32)

def sms24h(api_key, action, **params):
    url = f"{API_BASE}?api_key={api_key}&action={action}"
    for k, v in params.items(): url += f"&{k}={v}"
    req = urllib.request.Request(url)
    try:
        with urllib.request.urlopen(req, timeout=15) as r:
            return r.read().decode().strip()
    except urllib.error.HTTPError as e: return f"HTTP_ERROR:{e.code}"
    except Exception as e: return f"ERROR:{e}"

async def get_db():
    return await aiomysql.create_pool(**DB_CONFIG)

async def get_user_by_token(token):
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute("SELECT u.id, u.username, u.sms_api_key, u.is_admin, u.is_active FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.token = %s AND s.expires_at > NOW() AND u.is_active = 1", (token,))
            return await cur.fetchone()

async def get_user_by_id(uid):
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute("SELECT id, username, sms_api_key, is_admin, is_active FROM users WHERE id = %s", (uid,))
            return await cur.fetchone()

# ---- Auth middleware ----

def require_auth(f):
    async def wrapper(request):
        auth = request.headers.get("Authorization", "")
        if not auth.startswith("Bearer "):
            return web.json_response({"error": "missing token"}, status=401)
        token = auth[7:]
        user = await get_user_by_token(token)
        if not user:
            return web.json_response({"error": "invalid or expired token"}, status=401)
        request["user"] = {"id": user[0], "username": user[1], "sms_api_key": user[2], "is_admin": user[3]}
        return await f(request)
    return wrapper

def require_admin(f):
    @require_auth
    async def wrapper(request):
        if not request["user"]["is_admin"]:
            return web.json_response({"error": "admin only"}, status=403)
        return await f(request)
    return wrapper

# ---- Handlers ----

async def register_handler(request):
    try:
        body = await request.json()
    except:
        return web.json_response({"error": "invalid json"}, status=400)
    username = body.get("username", "").strip()
    password = body.get("password", "").strip()
    sms_key = body.get("sms_api_key", "").strip()
    if len(username) < 3 or len(password) < 4:
        return web.json_response({"error": "username min 3, password min 4"}, status=400)
    if not re.match(r'^[a-zA-Z0-9_]+$', username):
        return web.json_response({"error": "username only letters, numbers, underscore"}, status=400)
    pw_hash = hash_password(password)
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            try:
                await cur.execute("INSERT INTO users (username, password_hash, sms_api_key) VALUES (%s, %s, %s)", (username, pw_hash, sms_key))
                return web.json_response({"ok": True})
            except Exception as e:
                if "Duplicate" in str(e):
                    return web.json_response({"error": "username already exists"}, status=400)
                return web.json_response({"error": str(e)}, status=500)

async def login_handler(request):
    try:
        body = await request.json()
    except:
        return web.json_response({"error": "invalid json"}, status=400)
    username = body.get("username", "").strip()
    password = body.get("password", "").strip()
    pw_hash = hash_password(password)
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute("SELECT id, username, is_admin, is_active FROM users WHERE username = %s AND password_hash = %s", (username, pw_hash))
            user = await cur.fetchone()
            if not user:
                return web.json_response({"error": "invalid credentials"}, status=401)
            if not user[3]:
                return web.json_response({"error": "account disabled"}, status=403)
            token = gen_token()
            await cur.execute("INSERT INTO sessions (user_id, token, expires_at) VALUES (%s, %s, DATE_ADD(NOW(), INTERVAL 30 DAY))", (user[0], token))
            return web.json_response({"token": token, "username": user[1], "is_admin": bool(user[2])})

async def logout_handler(request):
    auth = request.headers.get("Authorization", "")
    if auth.startswith("Bearer "):
        token = auth[7:]
        async with pool.acquire() as conn:
            async with conn.cursor() as cur:
                await cur.execute("DELETE FROM sessions WHERE token = %s", (token,))
    return web.json_response({"ok": True})

@require_auth
async def me_handler(request):
    u = request["user"]
    return web.json_response({"id": u["id"], "username": u["username"], "is_admin": u["is_admin"]})

@require_auth
async def send_handler(request):
    try:
        body = await request.json()
    except:
        return web.json_response({"error": "invalid json"}, status=400)
    phone = body.get("phone", "")
    if not phone:
        return web.json_response({"error": "phone required"}, status=400)
    user_id = request["user"]["id"]
    payload = json.dumps({"token": request["user"]["username"], "phone": phone})
    wss = user_phones.get(user_id, set())
    if not wss:
        return web.json_response({"error": "phone offline"}, status=502)
    dead = set()
    for ws in wss:
        try: await ws.send_str(payload)
        except: dead.add(ws)
    wss -= dead
    user_phones[user_id] = wss
    return web.json_response({"ok": True, "sent": len(wss) - len(dead)})

async def ws_handler(request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)
    user_id = None
    try:
        async for msg in ws:
            if msg.type == web.WSMsgType.TEXT:
                try:
                    data = json.loads(msg.data)
                except:
                    continue
                if data.get("type") == "auth":
                    token = data.get("token", "")
                    user = await get_user_by_token(token)
                    if user:
                        user_id = user[0]
                        if user_id not in user_phones:
                            user_phones[user_id] = set()
                        user_phones[user_id].add(ws)
                        await ws.send_str(json.dumps({"type": "auth_ok", "username": user[1]}))
                    else:
                        await ws.send_str(json.dumps({"type": "auth_error"}))
                        break
                elif data.get("type") == "ping":
                    await ws.send_str("pong")
            elif msg.type == web.WSMsgType.ERROR:
                pass
    except asyncio.CancelledError:
        pass
    finally:
        if user_id and user_id in user_phones:
            user_phones[user_id].discard(ws)
            if not user_phones[user_id]:
                del user_phones[user_id]
    return ws

@require_auth
async def get_settings_handler(request):
    user_id = request["user"]["id"]
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute("SELECT bloco_a, bloco_b, rodadas, quick_btns FROM user_settings WHERE user_id = %s", (user_id,))
            row = await cur.fetchone()
            if not row:
                # Create default settings
                default = json.dumps([])
                await cur.execute("INSERT INTO user_settings (user_id, bloco_a, bloco_b, rodadas, quick_btns) VALUES (%s, %s, %s, %s, %s)", (user_id, default, default, json.dumps([80,80,30,10]), default))
                row = (default, default, json.dumps([80,80,30,10]), default)
    return web.json_response({
        "bloco": json.loads(row[0] or "[]"),
        "bloco_b": json.loads(row[1] or "[]"),
        "rodadas": json.loads(row[2] or "[80,80,30,10]"),
        "quick_btns": json.loads(row[3] or "[]")
    })

@require_auth
async def save_settings_handler(request):
    try:
        body = await request.json()
    except:
        return web.json_response({"error": "invalid json"}, status=400)
    user_id = request["user"]["id"]
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute("""
                INSERT INTO user_settings (user_id, bloco_a, bloco_b, rodadas, quick_btns)
                VALUES (%s, %s, %s, %s, %s)
                ON DUPLICATE KEY UPDATE bloco_a=VALUES(bloco_a), bloco_b=VALUES(bloco_b), rodadas=VALUES(rodadas), quick_btns=VALUES(quick_btns)
            """, (
                user_id,
                json.dumps(body.get("bloco", [])),
                json.dumps(body.get("bloco_b", [])),
                json.dumps(body.get("rodadas", [80,80,30,10])),
                json.dumps(body.get("quick_btns", []))
            ))
    return web.json_response({"ok": True})

@require_auth
async def update_api_key_handler(request):
    try:
        body = await request.json()
    except:
        return web.json_response({"error": "invalid json"}, status=400)
    sms_key = body.get("sms_api_key", "").strip()
    user_id = request["user"]["id"]
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute("UPDATE users SET sms_api_key = %s WHERE id = %s", (sms_key, user_id))
    return web.json_response({"ok": True, "has_key": bool(sms_key)})

@require_auth
async def get_api_key_status_handler(request):
    user_id = request["user"]["id"]
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute("SELECT sms_api_key != '' FROM users WHERE id = %s", (user_id,))
            row = await cur.fetchone()
    return web.json_response({"has_key": bool(row and row[0])})

@require_auth
async def get_history_handler(request):
    user_id = request["user"]["id"]
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute("SELECT type, data, created_at FROM history WHERE user_id = %s ORDER BY created_at DESC LIMIT 100", (user_id,))
            rows = await cur.fetchall()
    return web.json.json_response([{"type": r[0], "data": json.loads(r[1]) if r[1] else {}, "created_at": r[2].isoformat()} for r in rows])

@require_auth
async def add_history_handler(request):
    try:
        body = await request.json()
    except:
        return web.json_response({"error": "invalid json"}, status=400)
    user_id = request["user"]["id"]
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute("INSERT INTO history (user_id, type, data) VALUES (%s, %s, %s)", (user_id, body.get("type", "send"), json.dumps(body.get("data", {}))))
    return web.json_response({"ok": True})

@require_auth
async def sms24h_balance(request):
    u = request["user"]
    api_key = u.get("sms_api_key", "") or (await request.json()).get("api_key", "")
    if not api_key:
        # Try from body
        try:
            body = await request.json()
            api_key = body.get("api_key", "")
        except: pass
    if not api_key: return web.json_response({"error": "api_key required"}, status=400)
    r = sms24h(api_key, "getBalance")
    if r.startswith("ACCESS_BALANCE:"):
        return web.json_response({"balance": float(r.split(":")[1])})
    return web.json_response({"error": r}, status=400)

@require_auth
async def sms24h_services(request):
    u = request["user"]
    api_key = u.get("sms_api_key", "")
    try:
        body = await request.json()
        api_key = body.get("api_key", api_key)
    except: pass
    if not api_key: return web.json_response({"error": "api_key required"}, status=400)
    r = sms24h(api_key, "getPrices", country=COUNTRY_BR)
    try:
        data = json.loads(r)
        services = data.get(COUNTRY_BR, data)
        if isinstance(services, dict):
            result = []
            for code, info in sorted(services.items()):
                result.append({"code": code, "name": SERVICES_NOME.get(code, code), "cost": info.get("cost", 0), "count": info.get("count", 0)})
            return web.json_response({"services": result, "count": len(result)})
    except: pass
    return web.json_response({"error": "failed to parse services"}, status=400)

@require_auth
async def sms24h_buy(request):
    u = request["user"]
    api_key = u.get("sms_api_key", "")
    try:
        body = await request.json()
        api_key = body.get("api_key", api_key)
        service = body.get("service", "")
    except:
        return web.json_response({"error": "invalid json"}, status=400)
    if not api_key or not service: return web.json_response({"error": "api_key and service required"}, status=400)
    r = sms24h(api_key, "getNumber", service=service, country=COUNTRY_BR)
    if r.startswith("ACCESS_NUMBER:"):
        parts = r.split(":")
        return web.json_response({"activation_id": parts[1], "number": parts[2]})
    elif "NO_NUMBERS" in r: return web.json_response({"error": "Sem numeros disponiveis"}, status=400)
    elif "NO_BALANCE" in r: return web.json_response({"error": "Saldo insuficiente"}, status=400)
    else: return web.json_response({"error": r}, status=400)

@require_auth
async def sms24h_status(request):
    u = request["user"]
    api_key = u.get("sms_api_key", "")
    try:
        body = await request.json()
        api_key = body.get("api_key", api_key)
        activation_id = body.get("activation_id", "")
    except:
        return web.json_response({"error": "invalid json"}, status=400)
    if not api_key or not activation_id: return web.json_response({"error": "api_key and activation_id required"}, status=400)
    r = sms24h(api_key, "getStatus", id=activation_id)
    if r.startswith("STATUS_OK"):
        codigo = r.split(":")[1] if ":" in r else ""
        return web.json_response({"status": "OK", "code": codigo})
    elif "STATUS_WAIT_CODE" in r: return web.json_response({"status": "WAIT"})
    elif "STATUS_CANCEL" in r: return web.json_response({"status": "CANCEL"})
    else: return web.json_response({"status": "UNKNOWN", "raw": r})

@require_auth
async def sms24h_cancel(request):
    u = request["user"]
    api_key = u.get("sms_api_key", "")
    try:
        body = await request.json()
        api_key = body.get("api_key", api_key)
        activation_id = body.get("activation_id", "")
    except:
        return web.json_response({"error": "invalid json"}, status=400)
    if not api_key or not activation_id: return web.json_response({"error": "api_key and activation_id required"}, status=400)
    r = sms24h(api_key, "setStatus", id=activation_id, status="8")
    return web.json_response({"result": r})

@require_auth
async def health_handler(request):
    u = request["user"]
    online = u["id"] in user_phones and len(user_phones[u["id"]]) > 0
    return web.json_response({"status": "ok", "phone_connected": online})

# ---- Admin endpoints ----

@require_admin
async def admin_users_handler(request):
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute("SELECT id, username, is_admin, is_active, sms_api_key != '' as has_key, created_at FROM users ORDER BY id")
            rows = await cur.fetchall()
    result = []
    for r in rows:
        result.append({
            "id": r[0], "username": r[1], "is_admin": bool(r[2]),
            "is_active": bool(r[3]), "has_key": bool(r[4]),
            "online": r[0] in user_phones and len(user_phones.get(r[0], set())) > 0,
            "created_at": r[5].isoformat()
        })
    return web.json_response({"users": result})

@require_admin
async def admin_user_create_handler(request):
    try:
        body = await request.json()
    except:
        return web.json_response({"error": "invalid json"}, status=400)
    username = body.get("username", "").strip()
    password = body.get("password", "").strip()
    sms_key = body.get("sms_api_key", "").strip()
    if len(username) < 3 or len(password) < 4:
        return web.json_response({"error": "username min 3, password min 4"}, status=400)
    pw_hash = hash_password(password)
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            try:
                await cur.execute("INSERT INTO users (username, password_hash, sms_api_key) VALUES (%s, %s, %s)", (username, pw_hash, sms_key))
                return web.json_response({"ok": True})
            except Exception as e:
                return web.json_response({"error": str(e)}, status=400)

@require_admin
async def admin_user_toggle_handler(request):
    user_id = int(request.match_info.get("id", 0))
    if user_id == request["user"]["id"]:
        return web.json_response({"error": "cannot toggle yourself"}, status=400)
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute("UPDATE users SET is_active = NOT is_active WHERE id = %s", (user_id,))
            if cur.rowcount == 0:
                return web.json_response({"error": "user not found"}, status=404)
    return web.json_response({"ok": True})

# ---- App setup ----

app = web.Application()

# Public
app.router.add_post("/api/register", register_handler)
app.router.add_post("/api/login", login_handler)
app.router.add_post("/api/logout", logout_handler)

# Auth required
app.router.add_get("/api/me", me_handler)
app.router.add_post("/api/send", send_handler)
app.router.add_get("/ws", ws_handler)
app.router.add_get("/api/settings", get_settings_handler)
app.router.add_post("/api/settings", save_settings_handler)
app.router.add_get("/api/history", get_history_handler)
app.router.add_post("/api/history", add_history_handler)
app.router.add_get("/api/health", health_handler)
app.router.add_get("/api/settings/key", get_api_key_status_handler)
app.router.add_post("/api/settings/key", update_api_key_handler)
app.router.add_post("/api/sms24h/balance", sms24h_balance)
app.router.add_post("/api/sms24h/services", sms24h_services)
app.router.add_post("/api/sms24h/buy", sms24h_buy)
app.router.add_post("/api/sms24h/status", sms24h_status)
app.router.add_post("/api/sms24h/cancel", sms24h_cancel)

# Admin
app.router.add_get("/api/admin/users", admin_users_handler)
app.router.add_post("/api/admin/users", admin_user_create_handler)
app.router.add_post("/api/admin/users/{id}/toggle", admin_user_toggle_handler)

# Static files
static_dir = os.path.dirname(os.path.abspath(__file__))
app.router.add_static("/", static_dir, show_index=True)

async def startup(app):
    global pool
    pool = await aiomysql.create_pool(**DB_CONFIG)
    print("MySQL pool created")

async def shutdown(app):
    if pool:
        pool.close()
        await pool.wait_closed()

app.on_startup.append(startup)
app.on_shutdown.append(shutdown)

if __name__ == "__main__":
    print("PhoneSender VPS rodando em http://0.0.0.0:8080")
    print("WebSocket em ws://SEU_IP:8080/ws")
    print("Painel: https://celular.nexusmu.vip/painel_web.html")
    web.run_app(app, host="0.0.0.0", port=8080)
