*{margin:0;padding:0;box-sizing:border-box}body{font-family:Inter,-apple-system,sans-serif;background:#f5f7fa;color:#1a1a2e}.sb{position:fixed;left:0;top:0;bottom:0;width:230px;background:#fff;border-right:1px solid #e8ecf1;padding:24px 0;z-index:10}.logo{padding:0 20px 20px;font-size:20px;font-weight:700;border-bottom:1px solid #e8ecf1;margin-bottom:12px}.logo span{color:#4f46e5}.nav{display:flex;align-items:center;gap:10px;padding:10px 20px;color:#64748b;text-decoration:none;font-size:13px;font-weight:500;border-radius:6px;margin:2px 8px;transition:.15s}.nav:hover{background:#f1f5f9;color:#1a1a2e}.nav.ac{background:#eef2ff;color:#4f46e5}.main{margin-left:230px;padding:32px 40px}.card{background:#fff;border-radius:8px;border:1px solid #e8ecf1;margin-bottom:16px;overflow:hidden}.ct{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #e8ecf1}.ct h2{font-size:15px;font-weight:600}table{width:100%;border-collapse:collapse}th{text-align:left;padding:10px 20px;font-size:11px;font-weight:600;color:#94a3b8;text-transform:uppercase;background:#f8fafc}td{padding:12px 20px;font-size:13px;border-top:1px solid #f1f5f9}.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:6px;font-size:13px;font-weight:600;text-decoration:none;border:none;cursor:pointer;transition:.15s}.btn-p{background:#4f46e5;color:#fff}.btn-p:hover{background:#4338ca}.btn-g{background:#f1f5f9;color:#1a1a2e}.btn-g:hover{background:#e2e8f0}.btn-d{background:#fef2f2;color:#dc2626}.btn-d:hover{background:#fee2e2}.btn-s{font-size:11px;padding:4px 10px}.bp{background:#4f46e5;color:#fff;border:none}.bp:hover{background:#4338ca}.bd{background:#fef2f2;color:#dc2626;border:none}.bd:hover{background:#fee2e2}.bg{background:#f1f5f9;color:#1a1a2e;border:none}.bg:hover{background:#e2e8f0}.bs{font-size:11px;padding:4px 8px;border-radius:4px;cursor:pointer;text-decoration:none;display:inline-block}.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600}.ba{background:#eef2ff;color:#4f46e5}.bns{background:#fff7ed;color:#ea580c}.bg2{background:#ecfdf5;color:#059669}.by{background:#fefce8;color:#ca8a04}.br2{background:#fef2f2;color:#dc2626}.fg{margin-bottom:14px}.fg label{display:block;font-size:12px;font-weight:600;color:#64748b;margin-bottom:4px}.fg input,.fg select,.fg textarea{width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;font-family:Inter,sans-serif}.fg textarea{resize:vertical}.fr{display:flex;gap:12px}.fr .fg{flex:1}.mod{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:100;align-items:center;justify-content:center}.mod-box{background:#fff;border-radius:12px;padding:24px;width:480px;max-width:90vw;max-height:90vh;overflow-y:auto}.mod-box h3{font-size:16px;margin-bottom:16px}.g{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:20px}.st{background:#fff;border-radius:8px;border:1px solid #e8ecf1;padding:18px 20px;text-align:center}.st .l{font-size:11px;font-weight:600;color:#94a3b8;text-transform:uppercase;margin-bottom:6px}.st .v{font-size:28px;font-weight:700}.c1{color:#4f46e5}.c2{color:#059669}.c4{color:#ea580c}code{background:#f1f5f9;padding:2px 6px;border-radius:4px;font-size:12px}.msg{padding:12px 16px;border-radius:8px;margin-bottom:14px;font-size:13px}.me{background:#fef2f2;color:#dc2626;border:1px solid #fecaca}.mo{background:#ecfdf5;color:#059669;border:1px solid #a7f3d0}.logw{max-width:400px;margin:60px auto}.logb{background:#fff;border-radius:12px;padding:32px;border:1px solid #e8ecf1}.logb h1{font-size:20px;margin-bottom:4px}.logb p{color:#64748b;font-size:13px;margin-bottom:24px}.logb input{width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;margin-bottom:12px;font-family:Inter,sans-serif}.alt{text-align:center;margin-top:16px;font-size:13px;color:#94a3b8}.alt a{color:#4f46e5;text-decoration:none}'
JS = 'function delDom(id){if(confirm("Delete?"))fetch("/api/domain/del/"+id).then(()=>location.reload())}function delRec(id){if(confirm("Delete?"))fetch("/api/record/del/"+id,{method:"DELETE",credentials:"same-origin"}).then(r=>r.ok?location.reload():fetch("/admin/rec/"+id,{method:"DELETE",credentials:"same-origin"}).then(r2=>{if(r2.ok)location.reload();else alert("Permission denied")}))}function delGeo(id){fetch("/api/geo/"+id,{method:"DELETE",credentials:"same-origin"}).then(r=>r.ok?location.reload():fetch("/admin/geo/"+id,{method:"DELETE",credentials:"same-origin"}).then(()=>location.reload()))}function show(id){document.getElementById(id).style.display="flex"}function hide(e,id){if(e.target===e.currentTarget)document.getElementById(id).style.display="none"}'

def page(title, body, sidebar='', standalone=False):
    head = ('<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" '
            'content="width=device-width,initial-scale=1.0"><title>' + title +
            ' - dns118</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"><style>' +
            CSS + '</style></head><body>')
    if standalone:
        return head + body + '<script>' + JS + '</script></body></html>'
    return head + sidebar + '<div class="main">' + body + '</div><script>' + JS + '</script></body></html>'

# ====== LANDING PAGE HTML ======
LANDING = ('<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" '
    'content="width=device-width,initial-scale=1.0"><title>dns118.com - Free GeoDNS Service</title>'
    '<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">'
    '<style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:Inter,sans-serif;color:#1a1a2e;background:#fff}'
    '.topnav{padding:16px 40px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #f0f0f0}'
    '.topnav .logo{font-size:22px;font-weight:800}.topnav .logo span{color:#4f46e5}'
    '.topnav a{color:#64748b;text-decoration:none;font-size:14px;font-weight:500;margin-left:20px}'
    '.topnav .btn-sm{padding:8px 18px;border-radius:8px;font-size:13px;font-weight:600;background:#4f46e5;color:#fff;text-decoration:none}'
    '.hero{padding:80px 24px;text-align:center;max-width:720px;margin:0 auto}'
    '.hero h1{font-size:52px;font-weight:800;letter-spacing:-2px;margin-bottom:16px;line-height:1.1}'
    '.hero h1 span{background:linear-gradient(135deg,#4f46e5,#7c3aed);-webkit-background-clip:text;-webkit-text-fill-color:transparent}'
    '.hero p{font-size:18px;color:#64748b;margin-bottom:36px;line-height:1.6}'
    '.hero .btns{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}'
    '.btn{display:inline-flex;align-items:center;gap:6px;padding:14px 28px;border-radius:10px;font-size:15px;font-weight:600;text-decoration:none;transition:.2s}'
    '.btn-p{background:#4f46e5;color:#fff}.btn-p:hover{background:#4338ca;transform:translateY(-1px);box-shadow:0 4px 16px rgba(79,70,229,0.3)}'
    '.btn-g{background:#f1f5f9;color:#1a1a2e;border:1px solid #e2e8f0}.btn-g:hover{background:#e2e8f0}'
    '.features{padding:60px 24px;background:#f8fafc;text-align:center}'
    '.features h2{font-size:28px;margin-bottom:40px}'
    '.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:24px;max-width:960px;margin:0 auto}'
    '.fcard{background:#fff;border:1px solid #e8ecf1;border-radius:12px;padding:28px 20px;text-align:center}'
    '.fcard .icon{font-size:36px;margin-bottom:12px}.fcard h3{font-size:16px;margin-bottom:6px}'
    '.fcard p{font-size:13px;color:#64748b;line-height:1.5}'
    '.pricing{padding:60px 24px;text-align:center}.pricing h2{font-size:28px;margin-bottom:40px}'
    '.plans{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:20px;max-width:860px;margin:0 auto}'
    '.plan{background:#fff;border:2px solid #e8ecf1;border-radius:14px;padding:32px 24px;text-align:center;transition:.2s}'
    '.plan:hover{border-color:#4f46e5;transform:translateY(-2px)}'
    '.plan.pop{border-color:#4f46e5;box-shadow:0 4px 20px rgba(79,70,229,0.15)}'
    '.plan h3{font-size:18px;margin-bottom:8px}.plan .price{font-size:36px;font-weight:800;margin:16px 0}'
    '.plan .price sub{font-size:14px;color:#94a3b8}'
    '.plan ul{list-style:none;text-align:left;margin:20px 0;font-size:13px;color:#64748b}'
    '.plan ul li{padding:4px 0}.plan ul li::before{content:"\\2713 ";color:#4f46e5}'
    '.plan .btn{width:100%;justify-content:center}'
    '.footer{padding:24px;text-align:center;color:#94a3b8;font-size:12px;border-top:1px solid #f0f0f0}'
    '</style></head><body>'
    '<div class="topnav"><div class="logo">dns<span>118</span></div>'
    '<div><a href="/login">Sign In</a><a href="/register" class="btn-sm">Register</a></div></div>'
    '<div class="hero"><h1>Free <span>GeoDNS</span> for Everyone</h1>'
    '<p>Fast, reliable DNS hosting with country-based routing.<br>Free forever. No credit card required.</p>'
    '<div class="btns"><a href="/register" class="btn btn-p">Get Started Free</a>'
    '<a href="/login" class="btn btn-g">Sign In</a></div></div>'
    '<div class="features"><h2>Why dns118?</h2><div class="grid">'
    '<div class="fcard"><div class="icon">WW</div><h3>Global Anycast</h3><p>Tokyo, Hong Kong, US your DNS resolves from the nearest node.</p></div>'
    '<div class="fcard"><div class="icon">GT</div><h3>GEO Routing</h3><p>Route users by country. China goes to HK, US goes to LA. You control it.</p></div>'
    '<div class="fcard"><div class="icon">FL</div><h3>Fast and Free</h3><p>Powered by PowerDNS. Free tier with 1 domain, 50K queries per month.</p></div>'
    '<div class="fcard"><div class="icon">LC</div><h3>SSL Included</h3><p>Every domain gets free SSL. HTTPS for everyone.</p></div>'
    '<div class="fcard"><div class="icon">CH</div><h3>Analytics</h3><p>Track query stats per domain. Know where your traffic comes from.</p></div>'
    '<div class="fcard"><div class="icon">CD</div><h3>API Access</h3><p>Full REST API. Automate DNS management from your scripts.</p></div>'
    '</div></div><div class="pricing"><h2>Simple Pricing</h2><div class="plans">'
    '<div class="plan"><h3>Free</h3><div class="price">$0<sub>/mo</sub></div>'
    '<ul><li>1 Domain</li><li>10 Records</li><li>50K Queries/mo</li><li>Basic Support</li></ul>'
    '<a href="/register" class="btn btn-g">Start Free</a></div>'
    '<div class="plan pop"><h3>Standard</h3><div class="price">$2<sub>/mo</sub></div>'
    '<ul><li>10 Domains</li><li>200 Records</li><li>2M Queries/mo</li><li>GEO Routing</li><li>Priority Support</li></ul>'
    '<a href="/register" class="btn btn-p">Register Free</a></div>'
    '<div class="plan"><h3>Max</h3><div class="price">$5<sub>/mo</sub></div>'
    '<ul><li>Unlimited Domains</li><li>Unlimited Records</li><li>Unlimited Queries</li><li>GEO Routing</li><li>API Access</li><li>24/7 Support</li></ul>'
    '<a href="/register" class="btn btn-g">Register Free</a></div></div></div>'
    '<div class="footer">dns118.com &mdash; ns1.dns118.com ns2.dns118.com &mdash; 172.105.239.250 | Tokyo, Japan</div></body></html>')

# ====== ROUTES ======

@app.route('/')
def home():
    if request.host == 'admin.dns118.com':
        return redirect('/admin')
    if get_user():
        return redirect('/dashboard')
    return LANDING

@app.route('/register', methods=['GET','POST'])
def register():
    if get_user():
        return redirect('/dashboard')
    msg = ''
    if request.method == 'POST':
        if not csrf_ok():
            msg = '<div class="msg me">Form expired, please try again</div>'
        else:
            un = request.form.get('username','').strip()
            em = request.form.get('email','').strip()
            pw = request.form.get('password','')
            pw2 = request.form.get('password2','')
            if not un or not em or not pw:
                msg = '<div class="msg me">All fields required</div>'
            elif pw != pw2:
                msg = '<div class="msg me">Passwords do not match</div>'
            elif len(pw) < 6:
                msg = '<div class="msg me">Password must be 6+ characters</div>'
            elif q('SELECT id FROM users WHERE username=%s', (un,)):
                msg = '<div class="msg me">Username taken</div>'
            else:
                q('INSERT INTO users (username,email,password,plan) VALUES (%s,%s,%s,%s)',
                  (un, em, hash_pw(pw), 'free'))
                msg = '<div class="msg mo">Registration submitted! Await admin approval.</div>'
    form = ('<div class="logw"><div class="logb"><h1>Create Account</h1>'
            '<p>Free GeoDNS hosting. All applications reviewed.</p>' + msg +
            '<form method="POST">' + csrf_in() +
            '<input name="username" placeholder="Username" required>'
            '<input name="email" type="email" placeholder="Email" required>'
            '<input name="password" type="password" placeholder="Password (6+ chars)" required>'
            '<input name="password2" type="password" placeholder="Confirm password" required>'
            '<button class="btn btn-p" type="submit" style="width:100%">Register</button></form>'
            '<div class="alt">Already have an account? <a href="/login">Sign In</a></div></div></div>')
    return page('Register', form, standalone=True)

@app.route('/login', methods=['GET','POST'])
def login():
    if request.host == 'admin.dns118.com':
        e = ''
        if request.method == 'POST':
            if not csrf_ok():
                e = '<div class="msg me">Form expired, please try again</div>'
            else:
                ip = request.remote_addr or ''
                if not rl_check(ip):
                    e = '<div class="msg me">Too many attempts. Try again in 15 min.</div>'
                elif check_pw(request.form.get('password',''), ADMIN_PW_HASH):
                    r = make_response(redirect('/admin'))
                    r.set_cookie('auth', ADMIN_TOKEN, max_age=86400, httponly=True, secure=True, samesite='Lax')
                    return r
                else:
                    e = '<div class="msg me">Invalid password</div>'
        return page('Admin',
            '<div class="logw"><div class="logb"><h1>Admin Panel</h1><p>dns118.com management</p>' + e +
            '<form method="POST">' + csrf_in() +
            '<input type="password" name="password" placeholder="Password" autofocus>' +
            '<button class="btn btn-p" type="submit" style="width:100%">Sign In</button></form></div></div>',
            standalone=True)

    msg = ''
    u = get_user()
    if u:
        if u['status'] == 'approved': return redirect('/dashboard')
        elif u['status'] == 'pending': msg = '<div class="msg me">Account pending approval</div>'
        else: msg = '<div class="msg me">Account rejected</div>'

    if request.method == 'POST':
        if not csrf_ok():
            msg = '<div class="msg me">Form expired, please try again</div>'
        else:
            ip = request.remote_addr or ''
            if not rl_check(ip):
                msg = '<div class="msg me">Too many attempts. Try again in 15 min.</div>'
            else:
                un = request.form.get('username','')
                pw = request.form.get('password','')
                rows = q('SELECT id,password,status FROM users WHERE username=%s', (un,))
                if rows and check_pw(pw, rows[0][1]):
                    pw_hash = rows[0][1]
                    if len(pw_hash) == 64 and all(c in '0123456789abcdef' for c in pw_hash.lower()):
                        new_hash = hash_pw(pw)
                        q('UPDATE users SET password=%s WHERE id=%s', (new_hash, rows[0][0]))
                        pw_hash = new_hash
                    r = make_response(redirect('/dashboard'))
                    r.set_cookie('uid', str(rows[0][0]), max_age=86400*30, httponly=True, secure=True, samesite='Lax')
                    r.set_cookie('upw', pw_hash, max_age=86400*30, httponly=True, secure=True, samesite='Lax')
                    return r
                msg = '<div class="msg me">Invalid credentials</div>'
    form = ('<div class="logw"><div class="logb"><h1>Sign In</h1><p>Access your DNS zones</p>' + msg +
            '<form method="POST">' + csrf_in() +
            '<input name="username" placeholder="Username" required>' +
            '<input name="password" type="password" placeholder="Password" required>' +
            '<button class="btn btn-p" type="submit" style="width:100%">Sign In</button></form>' +
            '<div class="alt">New user? <a href="/register">Register</a></div></div></div>')
    return page('Login', form, standalone=True)

@app.route('/logout')
def logout():
    r = make_response(redirect('/'))
    r.delete_cookie('uid'); r.delete_cookie('upw'); r.delete_cookie('auth')
    return r

# ====== USER DASHBOARD ======

@app.route('/dashboard')
def dashboard():
    u = get_user()
    if not u: return redirect('/login')
    if u['status'] != 'approved':
        return page('Dashboard', '<div class="msg me">Account pending approval</div>', SIDEBAR_USER)
    rows = ''
    for r in q('SELECT id,name,type FROM domains WHERE user_id=%s ORDER BY name', (u['id'],)):
        if len(r) < 3: continue
        cnt = q('SELECT count(*) FROM records WHERE domain_id=%s', (r[0],))[0][0]
        rows += ('<tr><td style="font-weight:600">' + str(r[1]) + '</td><td>' + str(r[2]) +
                 '</td><td>' + str(cnt) + '</td><td><a href="/d/' + str(r[0]) +
                 '" class="btn btn-g btn-s">Manage</a> <button class="btn btn-d btn-s" onclick="delDom(' +
                 str(r[0]) + ')">Delete</button></td></tr>')
    if not rows:
        rows = '<tr><td colspan="4" style="text-align:center;padding:40px;color:#94a3b8">No domains yet</td></tr>'
    body = ('<div class="card"><div class="ct"><h2>My Domains</h2>'
            '<button class="btn btn-p btn-s" onclick="show(\'mAdd\')">+ Add</button></div>'
            '<table><thead><tr><th>Domain</th><th>Type</th><th>Records</th><th>Actions</th></tr></thead>'
            '<tbody>' + rows + '</tbody></table></div>')
    body += ('<div id="mAdd" class="mod" style="display:none" onclick="hide(event,\'mAdd\')">'
             '<div class="mod-box"><h3>Add Domain</h3><form method="POST" action="/api/domain/add">' +
             csrf_in() +
             '<div class="fg"><label>Domain Name</label><input name="name" placeholder="example.com" required></div>'
             '<div class="fg"><label>Type</label><select name="type"><option>NATIVE</option></select></div>'
             '<button class="btn btn-p" type="submit">Create</button></form></div></div>')
    plan = q('SELECT plan FROM users WHERE id=%s', (u['id'],))[0][0]
    body = ('<div class="g"><div class="st"><div class="l">Plan</div>'
            '<div class="v c1">' + str(plan).upper() + '</div></div></div>') + body
    return page('Dashboard', body, SIDEBAR_USER)

@app.route('/d/<int:did>')
def domain_detail(did):
    u = get_user()
    if not u: return redirect('/login')
    dn = q('SELECT name FROM domains WHERE id=%s AND user_id=%s', (did, u['id']))
    if not dn: return 'Not found', 404
    dname = dn[0][0]
    rows = ''
    for r in q('SELECT id,name,type,content,ttl FROM records WHERE domain_id=%s ORDER BY FIELD(type,'
               '\'SOA\',\'NS\',\'A\',\'AAAA\',\'CNAME\',\'MX\'),name', (did,)):
        if len(r) < 4: continue
        nm = r[1].replace(dname, '@') if r[1] == dname else r[1].replace('.' + dname, '')
        ttl = str(r[4]) if len(r) > 4 else '-'
        ct = r[3]
        if len(ct) > 50: ct = ct[:47] + '...'
        qc = q('SELECT count(*) FROM query_stats_detailed WHERE domain_id=%s AND qname=%s AND '
               'created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)', (did, r[1]))
        qn = str(qc[0][0]) if qc else '0'
        rows += ('<tr><td style="font-weight:600">' + nm + '</td><td>' + badge(r[2]) +
                 '</td><td><code style="color:#64748b">' + ct + '</code></td><td>' + ttl +
                 's</td><td style="color:#4f46e5;font-weight:600">' + qn +
                 '</td><td><button class="btn btn-d btn-s" onclick="delRec(' + str(r[0]) + ')">Delete</button></td></tr>')
    if not rows:
        rows = '<tr><td colspan="6" style="text-align:center;padding:40px;color:#94a3b8">No records</td></tr>'
    body = ('<a href="/dashboard" class="btn btn-g" style="margin-bottom:16px">Back</a>'
            '<div class="card"><div class="ct"><h2>' + dname +
            '</h2><button class="btn btn-p btn-s" onclick="show(\'mRec\')">+ Add Record</button></div>'
            '<table><thead><tr><th>Name</th><th>Type</th><th>Value</th><th>TTL</th><th>Queries</th><th></th></tr>'
            '</thead><tbody>' + rows + '</tbody></table></div>')
    body += ('<div id="mRec" class="mod" style="display:none" onclick="hide(event,\'mRec\')">'
             '<div class="mod-box"><h3>Add Record</h3><form method="POST" action="/api/record/add/' +
             str(did) + '">' + csrf_in() +
             '<div class="fr"><div class="fg"><label>Name</label><input name="name" value="@" required></div>'
             '<div class="fg"><label>Type</label><select name="type"><option>A</option><option>AAAA</option>'
             '<option>CNAME</option><option>MX</option><option>TXT</option><option>NS</option><option>GEO</option>'
             '</select></div></div><div class="fg" id="geo_cc" style="display:none"><label>Country</label>'
             '<select name="cc"><option value="CN">China</option><option value="US">US</option>'
             '<option value="JP">Japan</option><option value="KR">Korea</option><option value="*">Default</option>'
             '</select></div><div class="fg"><label>Value</label><input name="content" placeholder="IP or hostname" required></div>'
             '<div class="fg"><label>TTL (sec)</label><input name="ttl" value="300"></div>'
             '<button class="btn btn-p" type="submit">Add</button></form></div></div>')
    return page(dname, body, SIDEBAR_USER)

# ====== TICKETS ======

@app.route('/tickets')
def user_tickets():
    u = get_user()
    if not u: return redirect('/login')
    tbody = ''
    for t in q('SELECT id,subject,status,created_at FROM tickets WHERE user_id=%s ORDER BY id DESC', (u['id'],)):
        cls = 'bg2' if t[2] == 'open' else ''
        tbody += ('<tr><td><a href="/ticket/' + str(t[0]) + '">#' + str(t[0]) + ' ' + str(t[1]) +
                  '</a></td><td><span class="badge ' + cls + '">' + str(t[2]) +
                  '</span></td><td>' + str(t[3])[:10] + '</td></tr>')
    if not tbody:
        tbody = '<tr><td colspan="3" style="text-align:center;padding:20px;color:#94a3b8">No tickets</td></tr>'
    body = ('<div class="card"><div class="ct"><h2>My Tickets</h2>'
            '<a href="/ticket/new" class="btn bp bs">+ New Ticket</a></div>'
            '<table><thead><tr><th>Subject</th><th>Status</th><th>Date</th></tr></thead>'
            '<tbody>' + tbody + '</tbody></table></div>')
    return page('Tickets', body, SIDEBAR_USER)

@app.route('/ticket/new', methods=['GET','POST'])
def ticket_new():
    u = get_user()
    if not u: return redirect('/login')
    msg = ''
    if request.method == 'POST':
        if not csrf_ok():
            msg = '<div class="msg me">Form expired</div>'
        else:
            subj = request.form.get('subject','')
            msg2 = request.form.get('message','')
            if subj and msg2:
                q('INSERT INTO tickets (user_id,subject) VALUES (%s,%s)', (u['id'], subj))
                tid = q('SELECT MAX(id) FROM tickets WHERE user_id=%s', (u['id'],))[0][0]
                q('INSERT INTO ticket_replies (ticket_id,user_id,message) VALUES (%s,%s,%s)',
                  (tid, u['id'], msg2))
                return redirect('/tickets')
            msg = '<div class="msg me">All fields required</div>'
    body = ('<div class="card"><div class="ct"><h2>New Ticket</h2></div><div style="padding:20px">' + msg +
            '<form method="POST">' + csrf_in() +
            '<div class="fg"><label>Subject</label>'
            '<input name="subject" required style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;font-family:Inter,sans-serif">'
            '</div><div class="fg"><label>Message</label>'
            '<textarea name="message" rows="6" required style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;font-family:Inter,sans-serif;resize:vertical"></textarea>'
            '</div><button class="btn bp" type="submit">Submit</button></form></div></div>')
    return page('New Ticket', body, SIDEBAR_USER)

@app.route('/ticket/<int:tid>', methods=['GET','POST'])
def ticket_view(tid):
    u = get_user()
    is_adm = is_admin()
    if not u and not is_adm: return redirect('/login')
    if request.method == 'POST':
        if not csrf_ok(): return redirect('/ticket/' + str(tid))
        msg = request.form.get('message','')
        if msg:
            uid = u['id'] if u else None
            q('INSERT INTO ticket_replies (ticket_id,user_id,is_admin,message) VALUES (%s,%s,%s,%s)',
              (tid, uid, 1 if is_adm else 0, msg))
        return redirect('/ticket/' + str(tid))
    t = q('SELECT id,subject,status,user_id FROM tickets WHERE id=%s', (tid,))[0]
    uname = q('SELECT username FROM users WHERE id=%s', (t[3],))[0][0]
    replies = ''
    for r in q('SELECT is_admin,message,created_at FROM ticket_replies WHERE ticket_id=%s ORDER BY id', (tid,)):
        nm = 'Admin' if r[0] == '1' else uname
        replies += ('<div style="padding:12px;margin:8px 0;background:#f8fafc;border-radius:8px">'
                    '<div style="display:flex;justify-content:space-between;margin-bottom:6px">'
                    '<span class="badge ' + ('bp' if r[0] == '1' else 'bg') + '" style="font-size:10px">' + nm +
                    '</span><span style="font-size:11px;color:#94a3b8">' + str(r[2])[:16] + '</span></div>'
                    '<div>' + str(r[1]) + '</div></div>')
    body = ('<div class="card"><div class="ct"><h2>#' + str(t[0]) + ' ' + str(t[1]) +
            '</h2><span style="color:#94a3b8;font-size:13px">by ' + uname + '</span> '
            '<span class="badge ' + ('bg2' if t[2] == 'open' else '') + '">' + str(t[2]) +
            '</span></div><div style="padding:20px">' + replies +
            '<form method="POST" style="margin-top:16px">' + csrf_in() +
            '<textarea name="message" rows="5" placeholder="Reply..." required '
            'style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;'
            'font-size:14px;font-family:Inter,sans-serif;resize:vertical"></textarea>'
            '<button class="btn bp" type="submit" style="margin-top:8px">Send</button>'
            '</form></div></div>')
    if is_adm:
        body += '<a href="/admin/ticket/' + str(tid) + '/close" class="btn bd bs">Close</a> '
    body += '<a href="/tickets" class="btn bg">Back</a>'
    sidebar = SIDEBAR_ADMIN if is_adm else SIDEBAR_USER
    return page('Ticket', body, sidebar)

# ====== STATS ======

@app.route('/stats')
def user_stats():
    u = get_user()
    if not u: return redirect('/login')
    uq = q('SELECT COALESCE(SUM(qh.query_count),0) FROM query_hourly qh JOIN domains d '
           'ON qh.domain_id=d.id WHERE d.user_id=%s AND qh.hour >= DATE_SUB(NOW(), INTERVAL 24 HOUR)', (u['id'],))
    tq = str(uq[0][0]) if uq else '0'
    sc = q('SELECT r.name, COALESCE(qsd.country,%s), COUNT(qsd.id) as cnt FROM records r '
           'JOIN domains d ON r.domain_id=d.id LEFT JOIN query_stats_detailed qsd ON '
           'qsd.qname=r.name AND qsd.domain_id=r.domain_id AND qsd.created_at >= '
           'DATE_SUB(NOW(), INTERVAL 24 HOUR) WHERE d.user_id=%s GROUP BY r.name, qsd.country '
           'ORDER BY cnt DESC LIMIT 50', ('default', u['id']))
    sr = ''
    for s in sc:
        if len(s) >= 3:
            sr += '<tr><td>' + str(s[0]) + '</td><td>' + str(s[1]) + '</td><td style="color:#4f46e5;font-weight:600">' + str(s[2]) + '</td></tr>'
    if not sr: sr = '<tr><td colspan="3" style="text-align:center;padding:20px;color:#94a3b8">No data yet</td></tr>'
    body = '<div class="card"><div class="ct"><h2>My Query Stats (24h) ' + tq + ' total</h2></div><table><thead><tr><th>Subdomain</th><th>Country</th><th>Queries</th></tr></thead><tbody>' + sr + '</tbody></table></div>'
    return page('Stats', body, SIDEBAR_USER)

@app.route('/upgrade')
def upgrade_page():
    u = get_user()
    if not u: return redirect('/login')
    body = ('<div style="max-width:500px;margin:40px auto"><div class="card"><div class="ct"><h2>Upgrade Plan</h2></div>'
            '<div style="padding:20px"><h3>Standard $2/mo</h3>'
            '<ul style="margin:8px 0 16px 20px;font-size:13px;color:#64748b"><li>10 Domains</li>'
            '<li>200 Records</li><li>GEO Routing</li><li>API Access</li></ul>'
            '<a href="https://paypal.me/dns118/2" target="_blank" class="btn bp" '
            'style="display:block;text-align:center;text-decoration:none;margin-bottom:4px">Pay via PayPal</a>'
            '<p style="font-size:11px;color:#94a3b8;margin-bottom:20px">After payment submit a ticket to activate</p>'
            '<h3>Max $5/mo</h3>'
            '<ul style="margin:8px 0 16px 20px;font-size:13px;color:#64748b"><li>Unlimited Domains</li>'
            '<li>Unlimited Records</li><li>GEO Routing</li><li>API Access</li><li>Priority Support</li></ul>'
            '<a href="https://paypal.me/dns118/5" target="_blank" class="btn bp" '
            'style="display:block;text-align:center;text-decoration:none;margin-bottom:4px">Pay via PayPal</a>'
            '<p style="font-size:11px;color:#94a3b8">After payment submit a ticket to activate</p></div></div></div>')
    return page('Upgrade', body, SIDEBAR_USER)

@app.route('/changepw', methods=['GET','POST'])
def change_password():
    u = get_user()
    if not u: return redirect('/login')
    msg = ''
    if request.method == 'POST':
        if not csrf_ok():
            msg = '<div class="msg me">Form expired</div>'
        else:
            old = request.form.get('old','')
            new = request.form.get('new','')
            new2 = request.form.get('new2','')
            cur_hash = q('SELECT password FROM users WHERE id=%s', (u['id'],))[0][0]
            if not check_pw(old, cur_hash):
                msg = '<div class="msg me">Current password incorrect</div>'
            elif len(new) < 6:
                msg = '<div class="msg me">6+ chars required</div>'
            elif new != new2:
                msg = '<div class="msg me">Passwords do not match</div>'
            else:
                new_hash = hash_pw(new)
                q('UPDATE users SET password=%s WHERE id=%s', (new_hash, u['id']))
                msg = '<div class="msg mo">Password changed!</div>'
    body = ('<div style="max-width:440px;margin:40px auto"><div class="card"><div class="ct"><h2>Change Password</h2></div>'
            '<div style="padding:20px">' + msg +
            '<form method="POST">' + csrf_in() +
            '<div class="fg"><label>Current</label><input name="old" type="password" required></div>'
            '<div class="fg"><label>New</label><input name="new" type="password" required></div>'
            '<div class="fg"><label>Confirm</label><input name="new2" type="password" required></div>'
            '<button class="btn bp" type="submit">Update</button></form></div></div>'
            '<a href="/dashboard" class="btn bg" style="margin-top:12px">Back</a></div>')
    return page('Change Password', body, SIDEBAR_USER)

# ====== ADMIN ======

@app.route('/admin')
def admin_dash():
    if not is_admin():
        if request.host == 'admin.dns118.com': return redirect('/login')
        return 'Forbidden', 403
    uc = str(q('SELECT count(*) FROM users')[0][0])
    dc = str(q('SELECT count(*) FROM domains')[0][0])
    rc = str(q('SELECT count(*) FROM records')[0][0])
    b = '<div class="g"><div class="st"><div class="l">Users</div><div class="v c1">' + uc + '</div></div><div class="st"><div class="l">Domains</div><div class="v c4">' + dc + '</div></div><div class="st"><div class="l">Records</div><div class="v c2">' + rc + '</div></div></div>'
    dr = ''
    for d in q('SELECT d.id,d.name,u.username FROM domains d LEFT JOIN users u ON d.user_id=u.id ORDER BY d.name'):
        dr += ('<tr><td style="font-weight:600">' + str(d[1]) + '</td><td>' + str(d[2] or '-') +
               '</td><td>' + str(q('SELECT count(*) FROM records WHERE domain_id=%s', (d[0],))[0][0]) +
               '</td><td><a href="/admin/dom/' + str(d[0]) + '" class="btn bg bs">Manage</a></td></tr>')
    if not dr: dr = '<tr><td colspan="4" style="text-align:center;padding:36px;color:#94a3b8">No domains</td></tr>'
    b += '<div class="card"><div class="ct"><h2>All Domains</h2></div><table><thead><tr><th>Domain</th><th>Owner</th><th>Records</th><th></th></tr></thead><tbody>' + dr + '</tbody></table></div>'
    ur = ''
    for u2 in q('SELECT id,username,email,status,created_at FROM users ORDER BY created_at DESC'):
        p = q('SELECT plan FROM users WHERE id=%s', (u2[0],))[0][0]
        cls = 'bg2' if u2[3] == 'approved' else ('by' if u2[3] == 'pending' else 'br2')
        ur += ('<tr><td><a href="/admin/plan/' + str(u2[0]) + '/free" style="color:#ef4444">F</a> '
               '<a href="/admin/plan/' + str(u2[0]) + '/standard" style="color:#4f46e5">S</a> '
               '<a href="/admin/plan/' + str(u2[0]) + '/max" style="color:#059669">M</a></td><td>' +
               str(u2[1]) + '</td><td>' + str(u2[2]) + '</td><td><span class="badge ' + cls + '">' +
               str(u2[3]) + '</span></td><td>' + str(u2[4])[:10] + '</td><td>'
               '<a href="/admin/ok/' + str(u2[0]) + '" class="btn bp bs">Approve</a> '
               '<a href="/admin/no/' + str(u2[0]) + '" class="btn bd bs">Reject</a> '
               '<a href="/admin/deluser/' + str(u2[0]) + '" class="btn bd bs" onclick="return confirm(\'Delete user?\')">Del</a></td></tr>')
    b += '<div class="card"><div class="ct"><h2>Users</h2></div><table><thead><tr><th>Plan</th><th>Username</th><th>Email</th><th>Status</th><th>Created</th><th></th></tr></thead><tbody>' + ur + '</tbody></table></div>'
    return page('Admin', b, SIDEBAR_ADMIN)

@app.route('/admin/ok/<int:uid>')
def adm_ok(uid):
    if not is_admin(): return 'Forbidden', 403
    q('UPDATE users SET status=%s WHERE id=%s', ('approved', uid))
    return redirect('/admin')

@app.route('/admin/no/<int:uid>')
def adm_no(uid):
    if not is_admin(): return 'Forbidden', 403
    q('UPDATE users SET status=%s WHERE id=%s', ('rejected', uid))
    return redirect('/admin')

@app.route('/admin/deluser/<int:uid>')
def adm_deluser(uid):
    if not is_admin(): return 'Forbidden', 403
    q('DELETE FROM users WHERE id=%s', (uid,))
    return redirect('/admin')

@app.route('/admin/plan/<int:uid>/<plan>')
def adm_set_plan(uid, plan):
    if not is_admin(): return 'Forbidden', 403
    if plan in ('free', 'standard', 'max'):
        q('UPDATE users SET plan=%s WHERE id=%s', (plan, uid))
    return redirect('/admin')

@app.route('/admin/dom/<int:did>')
def adm_dom(did):
    if not is_admin(): return 'Forbidden', 403
    dn = q('SELECT name FROM domains WHERE id=%s', (did,))
    dname = str(dn[0][0]) if dn else '?'
    rows = ''
    for r in q('SELECT id,name,type,content,ttl FROM records WHERE domain_id=%s ORDER BY FIELD(type,'
               '\'SOA\',\'NS\',\'A\',\'AAAA\',\'CNAME\',\'MX\')', (did,)):
        qc = q('SELECT count(*) FROM query_stats_detailed WHERE domain_id=%s AND qname=%s AND '
               'created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)', (did, r[1]))
        qn = str(qc[0][0]) if qc else '0'
        rows += ('<tr><td>' + str(r[1]) + '</td><td><span class="badge ba">' + str(r[2]) +
                 '</span></td><td><code>' + str(r[3])[:45] + '</code></td><td>' + str(r[4]) +
                 's</td><td style="color:#4f46e5;font-weight:600">' + qn +
                 '</td><td><button class="btn bd bs" onclick="delRec(' + str(r[0]) + ')">Del</button></td></tr>')
    if not rows: rows = '<tr><td colspan="6" style="text-align:center;padding:36px;color:#94a3b8">No records</td></tr>'
    b = ('<a href="/admin" class="btn bg" style="margin-bottom:14px">Back</a>'
         '<div class="card"><div class="ct"><h2>' + dname +
         '</h2><button class="btn bp bs" onclick="show(\'mr\')">+ Add</button></div>'
         '<table><thead><tr><th>Name</th><th>Type</th><th>Value</th><th>TTL</th><th>Queries</th><th></th></tr>'
         '</thead><tbody>' + rows + '</tbody></table></div>')
    b += ('<div id="mr" class="mod" style="display:none" onclick="hide(event,\'mr\')">'
          '<div class="mod-box"><h3>Add Record</h3><form method="POST" action="/admin/rec/' +
          str(did) + '">' + csrf_in() +
          '<div class="fr"><div class="fg"><label>Name</label><input name="nm" value="@" required></div>'
          '<div class="fg"><label>Type</label><select name="tp"><option>A</option><option>AAAA</option>'
          '<option>CNAME</option><option>MX</option><option>TXT</option><option>NS</option><option>GEO</option>'
          '</select></div></div><div class="fg" id="geo_cc"><label>Country</label>'
          '<select name="cc"><option value="CN">China</option><option value="US">US</option>'
          '<option value="JP">Japan</option><option value="*">Default</option></select></div>'
          '<div class="fg"><label>Value</label><input name="ct" placeholder="IP/hostname" required></div>'
          '<div class="fg"><label>TTL</label><input name="tl" value="300"></div>'
          '<button class="btn bp" type="submit">Add</button></form></div></div>')
    return page(dname, b, SIDEBAR_ADMIN)

@app.route('/admin/rec/<int:did>', methods=['POST'])
def adm_rec(did):
    if not is_admin(): return 'Forbidden', 403
    if not csrf_ok(): return 'Invalid form', 400
    n = request.form.get('nm','')
    t = request.form.get('tp','A')
    c = request.form.get('ct','')
    tl = request.form.get('tl','300')
    if t == 'GEO':
        cc = request.form.get('cc','*')
        q('INSERT INTO geo_rules (domain_id,subdomain,country_code,target_ip) VALUES (%s,%s,%s,%s) '
          'ON DUPLICATE KEY UPDATE target_ip=%s', (did, n, cc, c, c))
    else:
        q('INSERT INTO records (domain_id,name,type,content,ttl) VALUES (%s,%s,%s,%s,%s)',
          (did, n, t, c, clean_int(tl, 300)))
    return redirect('/admin/dom/' + str(did))

# ====== ADMIN TICKETS & STATS ======

@app.route('/admin/tickets')
def admin_tickets():
    if not is_admin(): return 'Forbidden', 403
    tbody = ''
    for t in q('SELECT t.id,t.subject,t.status,u.username,t.created_at FROM tickets t JOIN users u ON t.user_id=u.id ORDER BY t.id DESC'):
        cls = 'bg2' if t[2] == 'open' else ''
        tbody += ('<tr><td><a href="/ticket/' + str(t[0]) + '">#' + str(t[0]) + ' ' + str(t[1]) +
                  '</a></td><td>' + str(t[3]) + '</td><td><span class="badge ' + cls + '">' +
                  str(t[2]) + '</span></td><td>' + str(t[4])[:10] + '</td></tr>')
    if not tbody: tbody = '<tr><td colspan="4" style="text-align:center;padding:20px;color:#94a3b8">No tickets</td></tr>'
    body = '<div class="card"><div class="ct"><h2>All Tickets</h2></div><table><thead><tr><th>Subject</th><th>User</th><th>Status</th><th>Date</th></tr></thead><tbody>' + tbody + '</tbody></table></div>'
    return page('Tickets', body, SIDEBAR_ADMIN)

@app.route('/admin/ticket/<int:tid>/close')
def admin_close_ticket(tid):
    if not is_admin(): return 'Forbidden', 403
    q('UPDATE tickets SET status=%s WHERE id=%s', ('closed', tid))
    return redirect('/ticket/' + str(tid))

@app.route('/admin/password', methods=['GET','POST'])
def admin_change_pw():
    if not is_admin(): return 'Forbidden', 403
    msg = ''
    if request.method == 'POST':
        if not csrf_ok():
            msg = '<div class="msg me">Form expired</div>'
        else:
            old = request.form.get('old','')
            new = request.form.get('new','')
            new2 = request.form.get('new2','')
            if not check_pw(old, ADMIN_PW_HASH):
                msg = '<div class="msg me">Current password incorrect</div>'
            elif len(new) < 6:
                msg = '<div class="msg me">6+ chars required</div>'
            elif new != new2:
                msg = '<div class="msg me">Passwords do not match</div>'
            else:
                set_admin_pw(new)
                msg = '<div class="msg mo">Password changed!</div>'
    body = ('<div style="max-width:440px;margin:40px auto"><div class="card"><div class="ct"><h2>Admin Password</h2></div>'
            '<div style="padding:20px">' + msg + '<form method="POST">' + csrf_in() +
            '<div class="fg"><label>Current</label><input name="old" type="password" required></div>'
            '<div class="fg"><label>New</label><input name="new" type="password" required></div>'
            '<div class="fg"><label>Confirm</label><input name="new2" type="password" required></div>'
            '<button class="btn bp" type="submit">Update</button></form></div></div></div>')
    return page('Password', body, SIDEBAR_ADMIN)

# ====== API ======

@app.route('/api/domain/add', methods=['POST'])
def api_domain_add():
    u = get_user()
    if not u: return redirect('/login')
    dn = request.form.get('name','').lower().strip()
    if not dn or '.' not in dn: return '<script>alert("Invalid domain");history.back()</script>'
    q('INSERT INTO domains (name,type,user_id) VALUES (%s,%s,%s)', (dn, 'NATIVE', u['id']))
    did = q('SELECT id FROM domains WHERE name=%s', (dn,))[0][0]
    soa = 'ns1.dns118.com admin.dns118.com 2026071901 3600 900 604800 86400