"""Facebook Page OAuth and configurable location-aware JSON listing collector.
No Facebook session scraping, password collection, proxies or CAPTCHA bypass.
"""
import os,sys,json,time,secrets,hashlib,hmac,base64,re,socket,ipaddress
from pathlib import Path
from functools import wraps
from urllib.parse import urlencode,urlparse
import requests,urllib3,certifi
from cryptography.fernet import Fernet
from flask import request,session,render_template,redirect,url_for,flash,abort,jsonify

DEFAULTS={'facebook_enabled':'0','facebook_app_id':'','facebook_version':'','public_url':'','facebook_secret':'','collector_enabled':'0','collector_url':'','collector_interval':'300','collector_queries':'3','collector_source':'Marketplace feed','collector_key':'','collector_auth_header':'Authorization','collector_auth_prefix':'Bearer ', 'collector_results_path':'data.listings','collector_params':json.dumps({'query':'{make} {model}','lat':'{lat}','lng':'{lon}','radius_miles':'{radius}','max_price':'{max_price}','min_year':'{min_year}','max_mileage':'{max_mileage}'}),'collector_mapping':json.dumps({k:k for k in ['source_id','make','model','year','price','mileage','city','lat','lon','title','seller','url']})}
SENSITIVE={'facebook_secret','collector_key'}
class IntegrationError(Exception):pass

def public_endpoint(url):
    p=urlparse(url)
    if p.scheme!='https' or not p.hostname or p.username or p.password or p.fragment or p.port not in [None,443]:raise IntegrationError('Use a public HTTPS endpoint on port 443 without credentials or fragments.')
    try:ips={x[4][0] for x in socket.getaddrinfo(p.hostname,443,type=socket.SOCK_STREAM)}
    except OSError:raise IntegrationError('Provider hostname could not be resolved.')
    if not ips or any(not ipaddress.ip_address(ip).is_global for ip in ips):raise IntegrationError('Provider must resolve only to public IP addresses.')
    return p,sorted(ips)[0]

def provider_get(url,params,header,value):
    # Pin the validated public IP while retaining TLS hostname verification.
    p,ip=public_endpoint(url);path=p.path or '/'
    query='&'.join(x for x in [p.query,urlencode(params)] if x)
    if query:path+='?'+query
    pool=urllib3.HTTPSConnectionPool(ip,port=443,server_hostname=p.hostname,assert_hostname=p.hostname,cert_reqs='CERT_REQUIRED',ca_certs=certifi.where(),timeout=urllib3.Timeout(connect=5,read=15))
    headers={'Host':p.hostname,'Accept':'application/json','User-Agent':'OasisAutoScout/3'}
    if value:headers[header]=value
    response=None
    try:
        response=pool.request('GET',path,headers=headers,redirect=False,retries=False,preload_content=False)
        if response.status!=200:raise IntegrationError('Provider returned HTTP '+str(response.status)+'. Check endpoint, credential and plan limits.')
        payload=response.read(2*1024*1024+1)
        if len(payload)>2*1024*1024:raise IntegrationError('Provider response exceeds 2 MB.')
        try:return json.loads(payload)
        except (ValueError,UnicodeError):raise IntegrationError('Provider did not return valid JSON.')
    except (urllib3.exceptions.HTTPError,OSError):raise IntegrationError('Provider request failed or timed out.')
    finally:
        if response:response.close()
        pool.close()

def lookup(obj,path):
    for part in path.split('.') if path else []:
        if isinstance(obj,dict):obj=obj.get(part)
        elif isinstance(obj,list) and part.isdigit():obj=obj[int(part)] if int(part)<len(obj) else None
        else:return None
    return obj

def map_listing(row,mapping):
    out={k:lookup(row,path) for k,path in mapping.items()}
    for k in ['source_id','make','model','city','seller']:
        if out.get(k) is None or str(out[k]).strip()=='':raise IntegrationError('Listing lacks '+k+'. Adjust response field mapping.')
        out[k]=str(out[k])
    out['url']=out.get('url') or '';out['title']=out.get('title') or 'Unknown'
    for k in ['year','price','mileage','lat','lon']:
        if out.get(k) is None:raise IntegrationError('Listing lacks '+k+'. Adjust response field mapping.')
    return out

def install(app):
    core=sys.modules.get('server') or sys.modules['__main__'];db=core.db
    keypath=core.DATA/'integration.key'
    if not keypath.exists():
        try:
            fd=os.open(keypath,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
            with os.fdopen(fd,'wb') as f:f.write(Fernet.generate_key())
        except FileExistsError:pass
    cipher=Fernet(keypath.read_bytes().strip())
    def encrypt(v):return cipher.encrypt(v.encode()).decode() if v else ''
    def decrypt(v):return cipher.decrypt(v.encode()).decode() if v else ''
    with db() as c:
        c.executescript('''CREATE TABLE IF NOT EXISTS integration_settings(key TEXT PRIMARY KEY,value TEXT);
        CREATE TABLE IF NOT EXISTS facebook_links(dealer_id INTEGER PRIMARY KEY REFERENCES dealers(id),fb_user_id TEXT,page_id TEXT,page_name TEXT,page_token TEXT,status TEXT,checked TEXT);
        CREATE TABLE IF NOT EXISTS oauth_attempts(dealer_id INTEGER PRIMARY KEY REFERENCES dealers(id),state_hash TEXT,created REAL,pending_pages TEXT,fb_user_id TEXT);
        CREATE TABLE IF NOT EXISTS monitoring(dealer_id INTEGER PRIMARY KEY REFERENCES dealers(id),enabled INTEGER DEFAULT 1);
        CREATE TABLE IF NOT EXISTS collector_state(id INTEGER PRIMARY KEY CHECK(id=1),last_run REAL DEFAULT 0,lease_until REAL DEFAULT 0,cursor INTEGER DEFAULT 0,last_error TEXT DEFAULT '',processed INTEGER DEFAULT 0,alerts INTEGER DEFAULT 0);
        INSERT OR IGNORE INTO collector_state(id) VALUES(1);
        CREATE TABLE IF NOT EXISTS collector_runs(id INTEGER PRIMARY KEY,started TEXT DEFAULT CURRENT_TIMESTAMP,status TEXT,processed INTEGER,alerts INTEGER,message TEXT);
        CREATE TABLE IF NOT EXISTS deletion_receipts(code TEXT PRIMARY KEY,created TEXT DEFAULT CURRENT_TIMESTAMP);''')
        for k,v in DEFAULTS.items():c.execute('INSERT OR IGNORE INTO integration_settings VALUES(?,?)',(k,v))
    def config(include_secrets=False):
        with db() as c:d={r['key']:r['value'] for r in c.execute('SELECT * FROM integration_settings')}
        if include_secrets:
            for k in SENSITIVE:d[k]=decrypt(d[k])
        else:
            for k in SENSITIVE:d[k]='Configured' if d[k] else ''
        return d
    def set_config(values):
        with db() as c:
            for k,v in values.items():c.execute('INSERT OR REPLACE INTO integration_settings VALUES(?,?)',(k,encrypt(v) if k in SENSITIVE else v))
            c.execute('INSERT INTO audit(actor,action) VALUES(?,?)',(session.get('dealer'),'Updated integration settings'))
    def admin_required(f):
        @wraps(f)
        def wrap(*a,**kw):
            if not core.is_admin():abort(403)
            return f(*a,**kw)
        return wrap
    def fb_ready(cfg):return cfg['facebook_enabled']=='1' and all(cfg[k] for k in ['facebook_app_id','facebook_version','public_url','facebook_secret'])
    def callback_url(cfg):return cfg['public_url'].rstrip('/')+'/facebook/callback'
    def graph(path,cfg,params=None,token=None):
        params=dict(params or {})
        if token:params['appsecret_proof']=hmac.new(cfg['facebook_secret'].encode(),token.encode(),hashlib.sha256).hexdigest()
        try:
            r=requests.get('https://graph.facebook.com/'+cfg['facebook_version']+'/'+path,params=params,headers={'Authorization':'Bearer '+token} if token else {},timeout=(5,15),allow_redirects=False)
            data=r.json()
            if r.status_code!=200 or 'error' in data:raise IntegrationError('Facebook request failed. Check app permissions, API version and authorization.')
            return data
        except (requests.RequestException,ValueError):raise IntegrationError('Facebook request failed or timed out.')

    @app.route('/admin/integrations',methods=['GET','POST'])
    @admin_required
    def admin_integrations():
        if request.method=='POST':
            try:
                current=config(True);vals={k:request.form.get(k,'').strip() for k in DEFAULTS if k not in SENSITIVE}
                # Preserve trailing whitespace in the auth prefix (e.g. "Bearer ").
                vals['collector_auth_prefix']=request.form.get('collector_auth_prefix','')
                for k in SENSITIVE:
                    if request.form.get('clear_'+k):vals[k]=''
                    elif request.form.get(k):vals[k]=request.form[k].strip()
                cfg={**current,**vals}
                if cfg['facebook_app_id'] and not cfg['facebook_app_id'].isdigit():raise IntegrationError('Facebook App ID must be numeric.')
                if cfg['facebook_version'] and not re.fullmatch(r'v\d+\.\d+',cfg['facebook_version']):raise IntegrationError('API version must use vNN.N format.')
                if cfg['public_url']:
                    p=urlparse(cfg['public_url'])
                    if p.scheme!='https' or not p.hostname or p.username or p.password or p.query or p.fragment or p.path not in ['', '/']:raise IntegrationError('Public URL must be the HTTPS subdomain origin.')
                vals['facebook_enabled']='1' if request.form.get('facebook_enabled')=='1' else '0'
                if vals['facebook_enabled']=='1' and not fb_ready({**cfg,'facebook_enabled':'1'}):raise IntegrationError('Fill all Facebook fields before enabling Login.')
                vals['collector_enabled']='1' if request.form.get('collector_enabled')=='1' else '0'
                for k,lo,hi in [('collector_interval',60,86400),('collector_queries',1,10)]:vals[k]=str(int(core.num(cfg,k,0,lo,hi)))
                if cfg['collector_url']:public_endpoint(cfg['collector_url'])
                if vals['collector_enabled']=='1' and not cfg['collector_url']:raise IntegrationError('Configure a listing API endpoint before enabling collection.')
                if not cfg['collector_source'] or len(cfg['collector_source'])>100:raise IntegrationError('Provide a source name up to 100 characters.')
                if not re.fullmatch(r'[A-Za-z][A-Za-z0-9-]{0,60}',cfg['collector_auth_header']) or cfg['collector_auth_header'].lower() in ['host','cookie','content-length']:raise IntegrationError('Choose a valid API credential header.')
                if '\n' in cfg['collector_auth_prefix'] or '\r' in cfg['collector_auth_prefix']:raise IntegrationError('Invalid credential prefix.')
                if any('\n' in cfg[k] or '\r' in cfg[k] for k in SENSITIVE):raise IntegrationError('Invalid credential.')
                if cfg['collector_results_path'] and not re.fullmatch(r'[A-Za-z0-9_.]+',cfg['collector_results_path']):raise IntegrationError('Results path must be a dotted JSON path.')
                params=json.loads(cfg['collector_params']);mapping=json.loads(cfg['collector_mapping'])
                if not isinstance(params,dict) or len(params)>30 or not isinstance(mapping,dict):raise IntegrationError('Parameters and mapping must be JSON objects.')
                sample={'make':'Toyota','model':'Camry','lat':36.16,'lon':-86.78,'radius':100,'max_price':30000,'max_mileage':80000,'min_year':2020}
                for k,v in params.items():
                    if not isinstance(v,str) or not isinstance(k,str):raise IntegrationError('Query parameter names and templates must be text.')
                    v.format_map(sample)
                required={'source_id','make','model','year','price','mileage','city','lat','lon','seller'}
                if not required<=mapping.keys() or any(not isinstance(v,str) or not re.fullmatch(r'[A-Za-z0-9_.]+',v) for v in mapping.values()):raise IntegrationError('Map every required vehicle field using dotted paths.')
                if any(len(v)>12000 for v in vals.values()):raise IntegrationError('Setting is too long.')
                set_config(vals);flash('Integration settings saved.');return redirect(url_for('admin_integrations'))
            except (IntegrationError,ValueError,TypeError,KeyError,AttributeError) as e:
                flash(str(e) if not isinstance(e,KeyError) else 'Unsupported parameter template placeholder.')
        with db() as c:state=c.execute('SELECT * FROM collector_state WHERE id=1').fetchone();runs=c.execute('SELECT * FROM collector_runs ORDER BY id DESC LIMIT 20').fetchall()
        return render_template('integrations.html',config=config(),state=state,runs=runs)

    @app.get('/facebook/start')
    @core.login_required
    def facebook_start():
        cfg=config(True)
        if not fb_ready(cfg):flash('Facebook Login has not been configured by the administrator.');return redirect(url_for('connections'))
        state=secrets.token_urlsafe(32)
        with db() as c:
            c.execute('DELETE FROM oauth_attempts WHERE created<?',(time.time()-600,))
            c.execute('INSERT OR REPLACE INTO oauth_attempts(dealer_id,state_hash,created,pending_pages,fb_user_id) VALUES(?,?,?,NULL,NULL)',(session['dealer'],hashlib.sha256(state.encode()).hexdigest(),time.time()))
        params={'client_id':cfg['facebook_app_id'],'redirect_uri':callback_url(cfg),'response_type':'code','scope':'pages_show_list,pages_read_engagement','state':state}
        return redirect('https://www.facebook.com/'+cfg['facebook_version']+'/dialog/oauth?'+urlencode(params))

    @app.get('/facebook/callback')
    @core.login_required
    def facebook_callback():
        cfg=config(True)
        if not fb_ready(cfg):abort(400)
        with db() as c:
            row=c.execute('SELECT * FROM oauth_attempts WHERE dealer_id=?',(session['dealer'],)).fetchone()
            if not row or row['created']<time.time()-600 or not secrets.compare_digest(row['state_hash'],hashlib.sha256(request.args.get('state','').encode()).hexdigest()):abort(400)
            # Consume state before exchanging authorization code to prevent replay.
            c.execute('DELETE FROM oauth_attempts WHERE dealer_id=?',(session['dealer'],))
        if request.args.get('error'):flash('Facebook authorization was declined.');return redirect(url_for('connections'))
        code=request.args.get('code')
        if not code:abort(400)
        try:
            short=graph('oauth/access_token',cfg,{'client_id':cfg['facebook_app_id'],'client_secret':cfg['facebook_secret'],'redirect_uri':callback_url(cfg),'code':code})['access_token']
            token=graph('oauth/access_token',cfg,{'grant_type':'fb_exchange_token','client_id':cfg['facebook_app_id'],'client_secret':cfg['facebook_secret'],'fb_exchange_token':short})['access_token']
            user=graph('me',cfg,{'fields':'id'},token)['id'];pages=[];after=None
            for _ in range(5):
                params={'fields':'id,name,access_token','limit':100}
                if after:params['after']=after
                data=graph('me/accounts',cfg,params,token)
                pages.extend(x for x in data.get('data',[]) if x.get('id') and x.get('name') and x.get('access_token'))
                after=data.get('paging',{}).get('cursors',{}).get('after')
                if not data.get('paging',{}).get('next') or not after:break
            if not pages:raise IntegrationError('No eligible Pages returned. Check Page access and granted permissions.')
            with db() as c:c.execute('INSERT INTO oauth_attempts(dealer_id,state_hash,created,pending_pages,fb_user_id) VALUES(?,?,?,?,?)',(session['dealer'],'consumed',time.time(),encrypt(json.dumps(pages)),str(user)))
            return redirect(url_for('facebook_select'))
        except (IntegrationError,KeyError,TypeError):flash('Unable to connect Facebook. Check app settings, Page access and permissions, then reconnect.');return redirect(url_for('connections'))

    @app.route('/facebook/select',methods=['GET','POST'])
    @core.login_required
    def facebook_select():
        with db() as c:row=c.execute('SELECT * FROM oauth_attempts WHERE dealer_id=?',(session['dealer'],)).fetchone()
        if not row or not row['pending_pages'] or row['created']<time.time()-600:flash('Page selection expired. Please reconnect.');return redirect(url_for('connections'))
        pages=json.loads(decrypt(row['pending_pages']))
        if request.method=='POST':
            page=next((p for p in pages if p['id']==request.form.get('page_id')),None)
            if not page:abort(400)
            with db() as c:
                c.execute('INSERT OR REPLACE INTO facebook_links VALUES(?,?,?,?,?,?,CURRENT_TIMESTAMP)',(session['dealer'],row['fb_user_id'],page['id'],page['name'],encrypt(page['access_token']),'Connected'))
                c.execute('DELETE FROM oauth_attempts WHERE dealer_id=?',(session['dealer'],))
                c.execute('INSERT OR IGNORE INTO connections(dealer_id) VALUES(?)',(session['dealer'],))
                c.execute('UPDATE connections SET page_id=?,page_url=? WHERE dealer_id=?',(page['id'],'https://www.facebook.com/'+page['id'],session['dealer']))
            flash('Facebook Page connected. Listing monitoring uses the separate configured data source.');return redirect(url_for('connections'))
        return render_template('facebook_select.html',pages=[{'id':p['id'],'name':p['name']} for p in pages])

    @app.post('/facebook/disconnect')
    @core.login_required
    def facebook_disconnect():
        with db() as c:
            c.execute('DELETE FROM facebook_links WHERE dealer_id=?',(session['dealer'],));c.execute('DELETE FROM oauth_attempts WHERE dealer_id=?',(session['dealer'],));c.execute("UPDATE connections SET page_id='',page_url='' WHERE dealer_id=?",(session['dealer'],))
        flash('Facebook data removed from this portal. You can revoke app permissions in Facebook Business Integrations.');return redirect(url_for('connections'))

    @app.post('/facebook/check')
    @core.login_required
    def facebook_check():
        with db() as c:row=c.execute('SELECT * FROM facebook_links WHERE dealer_id=?',(session['dealer'],)).fetchone()
        if not row:abort(404)
        try:
            data=graph(row['page_id'],config(True),{'fields':'id,name'},decrypt(row['page_token']))
            with db() as c:c.execute("UPDATE facebook_links SET status='Connected',page_name=?,checked=CURRENT_TIMESTAMP WHERE dealer_id=?",(data['name'],session['dealer']))
            flash('Facebook Page authorization verified.')
        except (IntegrationError,KeyError):
            with db() as c:c.execute("UPDATE facebook_links SET status='Reconnect required',checked=CURRENT_TIMESTAMP WHERE dealer_id=?",(session['dealer'],))
            flash('Page verification failed. Reconnect to renew authorization.')
        return redirect(url_for('connections'))

    def signed_user():
        try:
            a,b=request.form['signed_request'].split('.',1)
            decode=lambda v:base64.urlsafe_b64decode(v+'='*(-len(v)%4))
            secret=config(True)['facebook_secret']
            if not secret:abort(401)
            signature=hmac.new(secret.encode(),b.encode(),hashlib.sha256).digest()
            if not hmac.compare_digest(decode(a),signature):abort(401)
            data=json.loads(decode(b))
            if data.get('algorithm','').upper()!='HMAC-SHA256' or not data.get('user_id'):abort(401)
            return str(data['user_id'])
        except (ValueError,KeyError,TypeError):abort(401)
    def delete_fb_user(user):
        with db() as c:
            ids=[r['dealer_id'] for r in c.execute('SELECT dealer_id FROM facebook_links WHERE fb_user_id=?',(user,))]
            ids.extend(r['dealer_id'] for r in c.execute('SELECT dealer_id FROM oauth_attempts WHERE fb_user_id=?',(user,)))
            for id in set(ids):
                c.execute("UPDATE connections SET page_id='',page_url='' WHERE dealer_id=?",(id,))
                c.execute('DELETE FROM facebook_links WHERE dealer_id=?',(id,));c.execute('DELETE FROM oauth_attempts WHERE dealer_id=?',(id,))
    @app.post('/facebook/deauthorize')
    def facebook_deauthorize():delete_fb_user(signed_user());return '',200
    @app.post('/facebook/delete-data')
    def facebook_delete_data():
        delete_fb_user(signed_user());code=secrets.token_urlsafe(24)
        with db() as c:c.execute('INSERT INTO deletion_receipts(code) VALUES(?)',(code,))
        return jsonify(url=config()['public_url']+'/facebook/deletion-status/'+code,confirmation_code=code)
    @app.get('/facebook/deletion-status/<code>')
    def facebook_deletion_status(code):
        with db() as c:r=c.execute('SELECT 1 FROM deletion_receipts WHERE code=?',(code,)).fetchone()
        if not r:abort(404)
        return 'Facebook connection data deleted from this portal.',200

    @app.post('/monitoring')
    @core.login_required
    def monitoring_toggle():
        with db() as c:c.execute('INSERT INTO monitoring(dealer_id,enabled) VALUES(?,?) ON CONFLICT(dealer_id) DO UPDATE SET enabled=excluded.enabled',(session['dealer'],1 if request.form.get('enabled')=='1' else 0))
        flash('Monitoring preferences saved.');return redirect(url_for('connections'))
    @app.context_processor
    def integration_context():
        with db() as c:
            page=c.execute('SELECT page_id,page_name,status,checked FROM facebook_links WHERE dealer_id=?',(session.get('dealer'),)).fetchone()
            pref=c.execute('SELECT enabled FROM monitoring WHERE dealer_id=?',(session.get('dealer'),)).fetchone()
        cfg=config()
        return {'fb_page':page,'fb_enabled':cfg['facebook_enabled']=='1','monitoring_enabled':bool(not pref or pref['enabled']),'collector_enabled':cfg['collector_enabled']=='1'}

    def collect(force=False):
        cfg=config(True);now=time.time()
        if cfg['collector_enabled']!='1':return {'status':'disabled','processed':0,'alerts':0}
        with db() as c:
            c.execute('BEGIN IMMEDIATE');state=c.execute('SELECT * FROM collector_state WHERE id=1').fetchone()
            if state['lease_until']>now:return {'status':'busy','processed':0,'alerts':0}
            if not force and state['last_run']+int(cfg['collector_interval'])>now:return {'status':'not_due','processed':0,'alerts':0}
            c.execute('UPDATE collector_state SET lease_until=?,last_run=? WHERE id=1',(now+600,now))
            interests=c.execute('SELECT i.* FROM interests i JOIN dealers d ON d.id=i.dealer_id LEFT JOIN monitoring m ON m.dealer_id=d.id WHERE d.active=1 AND COALESCE(m.enabled,1)=1 ORDER BY i.id').fetchall()
        processed=alerts=0;errors=[];cursor=state['cursor']
        try:
            params_config=json.loads(cfg['collector_params']);mapping=json.loads(cfg['collector_mapping'])
            ordered=[i for i in interests if i['id']>cursor]+[i for i in interests if i['id']<=cursor]
            for i in ordered[:int(cfg['collector_queries'])]:
                cursor=i['id']
                try:
                    params={k:v.format_map(dict(i)) for k,v in params_config.items()}
                    data=provider_get(cfg['collector_url'],params,cfg['collector_auth_header'],cfg['collector_auth_prefix']+cfg['collector_key'] if cfg['collector_key'] else '')
                    rows=lookup(data,cfg['collector_results_path'])
                    if not isinstance(rows,list) or len(rows)>500:raise IntegrationError('Response results must be an array of at most 500 listings; check results path or provider limit parameter.')
                    vehicles=[map_listing(r,mapping) for r in rows]
                    n,a=core.import_rows(vehicles,cfg['collector_source']);processed+=n;alerts+=a
                except IntegrationError as e:errors.append('Interest '+str(i['id'])+': '+str(e))
                except (ValueError,TypeError,KeyError):errors.append('Interest '+str(i['id'])+': invalid provider data. Check numeric fields, parameter templates and response mapping.')
        except (ValueError,TypeError):errors.append('Invalid collector configuration.')
        finally:
            status='error' if errors else 'ok'
            with db() as c:
                c.execute('UPDATE collector_state SET lease_until=0,cursor=?,last_error=?,processed=?,alerts=? WHERE id=1',(cursor,' '.join(errors)[:2000],processed,alerts))
                c.execute('INSERT INTO collector_runs(status,processed,alerts,message) VALUES(?,?,?,?)',(status,processed,alerts,' '.join(errors)[:2000] or ('No active interests.' if not interests else 'Completed')))
                c.execute('DELETE FROM collector_runs WHERE id NOT IN (SELECT id FROM collector_runs ORDER BY id DESC LIMIT 200)')
                c.execute('DELETE FROM oauth_attempts WHERE created<?',(time.time()-600,))
        return {'status':status,'processed':processed,'alerts':alerts}
    core.collect=collect;core.integration_config=config
    @app.post('/admin/collector/run')
    @admin_required
    def collector_run():
        result=collect(force=True);flash('Collector: '+result['status']+f" · {result['processed']} listings · {result['alerts']} new alerts");return redirect(url_for('admin_integrations'))
