"""Operational configuration lives in SQLite and is editable in the app."""
import json,secrets,os,re,getpass
from functools import wraps
from urllib.parse import urlparse
from flask import request,session,render_template,redirect,url_for,flash,abort
from werkzeug.security import generate_password_hash,check_password_hash
DEFAULTS={'brand_name':'Oasis AutoScout','hero_title':'Find the inventory your dealership needs.','hero_text':'Search available vehicles, save your buying criteria, and turn new matches into acquisition leads.','footer_text':'The Oasis Enterprise · Vehicle acquisition tools','accent_color':'#087f8c','oasis_url':'https://theoasisenterprise.com','support_email':'','registration_open':'1','max_radius':'500','default_radius':'100','default_lat':'36.16','default_lon':'-86.78','max_interests':'50','worker_interval':'30','lead_statuses':'Saved\nContacted\nNegotiating\nInspection\nPurchased\nRejected'}

def install(app):
    # Import the active application module without duplicating it when run as a script.
    import sys
    core=sys.modules.get('server') or sys.modules['__main__']
    db=core.db
    with db() as c:
        cols=[r['name'] for r in c.execute('PRAGMA table_info(dealers)')]
        if 'role' not in cols: c.execute("ALTER TABLE dealers ADD COLUMN role TEXT DEFAULT 'dealer'")
        if 'active' not in cols: c.execute('ALTER TABLE dealers ADD COLUMN active INTEGER DEFAULT 1')
        c.executescript('''CREATE TABLE IF NOT EXISTS app_settings(key TEXT PRIMARY KEY,value TEXT NOT NULL);
        CREATE TABLE IF NOT EXISTS sources(id INTEGER PRIMARY KEY,name TEXT UNIQUE NOT NULL,enabled INTEGER DEFAULT 1,token_hash TEXT NOT NULL,created TEXT DEFAULT CURRENT_TIMESTAMP);
        CREATE TABLE IF NOT EXISTS audit(id INTEGER PRIMARY KEY,actor INTEGER,action TEXT,created TEXT DEFAULT CURRENT_TIMESTAMP);''')
        for k,v in DEFAULTS.items(): c.execute('INSERT OR IGNORE INTO app_settings VALUES(?,?)',(k,v))
        legacy=os.getenv('INGEST_TOKEN','')
        if legacy and not c.execute('SELECT 1 FROM sources').fetchone():c.execute('INSERT INTO sources(name,token_hash) VALUES(?,?)',('Initial source',generate_password_hash(legacy)))
    def all_settings():
        with db() as c:return {r['key']:r['value'] for r in c.execute('SELECT * FROM app_settings')}
    def setting(k):return all_settings().get(k,DEFAULTS.get(k,''))
    def lead_statuses():return setting('lead_statuses').splitlines()
    def is_admin():
        with db() as c:r=c.execute('SELECT role,active FROM dealers WHERE id=?',(session.get('dealer'),)).fetchone()
        return bool(r and r['role']=='admin' and r['active'])
    def admin_required(f):
        @wraps(f)
        def wrap(*a,**kw):
            if not is_admin():abort(403)
            return f(*a,**kw)
        return wrap
    def log(c,action):c.execute('INSERT INTO audit(actor,action) VALUES(?,?)',(session.get('dealer'),action))
    def authorized_source(token):
        if not token:return None
        with db() as c:
            for r in c.execute('SELECT * FROM sources WHERE enabled=1'):
                if check_password_hash(r['token_hash'],token):return r['name']
        return None
    def import_rows(rows,source):
        if not isinstance(rows,list) or len(rows)>500:raise ValueError('Expected array of up to 500 listings')
        with db() as c:
            for v in rows:
                if not isinstance(v,dict):raise ValueError('Invalid listing')
                for k in ('source_id','make','model','city','seller'):
                    if not isinstance(v.get(k),str) or not v[k].strip() or len(v[k])>500:raise ValueError('Invalid '+k)
                for k in ('year','price','mileage','lat','lon'):
                    if k not in v:raise ValueError('Missing '+k)
                u=v.get('url','');parsed=urlparse(u)
                if u and (parsed.scheme!='https' or not parsed.netloc):raise ValueError('Listing URL must use HTTPS')
                values=(source+':'+v['source_id'],v['make'],v['model'],int(core.num(v,'year',2020,1900,2100)),int(core.num(v,'price')),int(core.num(v,'mileage')),v['city'],core.num(v,'lat',0,-90,90),core.num(v,'lon',0,-180,180),str(v.get('title','Unknown'))[:100],v['seller'],u,source)
                c.execute('INSERT INTO vehicles(source_id,make,model,year,price,mileage,city,lat,lon,title,seller,url,source) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(source_id) DO UPDATE SET make=excluded.make,model=excluded.model,year=excluded.year,price=excluded.price,mileage=excluded.mileage,city=excluded.city,lat=excluded.lat,lon=excluded.lon,title=excluded.title,seller=excluded.seller,url=excluded.url',values)
            n=core.process_matches(c)
        return len(rows),n
    for name,value in locals().copy().items():
        if name in ['all_settings','setting','lead_statuses','is_admin','authorized_source','import_rows']:setattr(core,name,value)

    @app.cli.command('create-admin')
    def create_admin():
        """One-time administrator setup, never grant admin to public registrants."""
        email=input('Admin email: ').strip().lower();name=input('Admin name: ').strip();pw=getpass.getpass('Password (12+ characters): ')
        if not re.fullmatch(r'[^\s@]+@[^\s@]+\.[^\s@]+',email) or not name or len(pw)<12:raise ValueError('Invalid account details')
        with db() as c:
            if c.execute('SELECT 1 FROM dealers WHERE email=?',(email,)).fetchone():raise ValueError('Email already exists. Use a separate admin email.')
            c.execute("INSERT INTO dealers(name,email,password,role) VALUES(?,?,?,'admin')",(name,email,generate_password_hash(pw)))
        print('Administrator created. Sign in through the portal.')

    @app.route('/admin',methods=['GET','POST'])
    @admin_required
    def admin_settings():
        if request.method=='POST':
            try:
                values={k:request.form.get(k,'').strip() for k in DEFAULTS}
                for k,lo,hi in [('max_radius',1,5000),('default_radius',1,5000),('max_interests',1,1000),('worker_interval',10,3600)]:values[k]=str(int(core.num(values,k,0,lo,hi)))
                core.num(values,'default_lat',0,-90,90);core.num(values,'default_lon',0,-180,180)
                if int(values['default_radius'])>int(values['max_radius']):raise ValueError('Default radius must not exceed maximum')
                if not re.fullmatch(r'#[0-9a-fA-F]{6}',values['accent_color']):raise ValueError('Choose a valid accent color')
                if values['oasis_url'] and (urlparse(values['oasis_url']).scheme!='https' or not urlparse(values['oasis_url']).netloc):raise ValueError('Oasis URL must use HTTPS')
                if not values['brand_name'] or not values['hero_title']:raise ValueError('Brand and headline are required')
                statuses=list(dict.fromkeys(x.strip() for x in values['lead_statuses'].splitlines() if x.strip()))
                if not statuses or len(statuses)>20 or any(len(x)>50 for x in statuses):raise ValueError('Enter 1–20 lead statuses, up to 50 characters each')
                values['lead_statuses']='\n'.join(statuses);values['registration_open']='1' if request.form.get('registration_open')=='1' else '0'
                if any(len(v)>5000 for v in values.values()):raise ValueError('Setting too long')
                with db() as c:
                    c.executemany('INSERT OR REPLACE INTO app_settings VALUES(?,?)',values.items());log(c,'Updated application settings')
                flash('Settings updated. Changes apply immediately.');return redirect(url_for('admin_settings'))
            except (ValueError,TypeError) as e:flash(str(e))
        return render_template('admin.html',config=all_settings())

    @app.route('/admin/sources',methods=['GET','POST'])
    @admin_required
    def admin_sources():
        token=None
        if request.method=='POST':
            name=request.form.get('name','').strip()
            if not name or len(name)>100:flash('Source name is required (100 characters maximum).')
            else:
                token=secrets.token_urlsafe(40)
                try:
                    with db() as c:c.execute('INSERT INTO sources(name,token_hash) VALUES(?,?)',(name,generate_password_hash(token)));log(c,'Added listing source '+name)
                except core.sqlite3.IntegrityError:flash('Source name already exists.');token=None
        with db() as c: sources=c.execute('SELECT id,name,enabled,created FROM sources').fetchall()
        return render_template('sources.html',sources=sources,token=token)

    @app.post('/admin/sources/<int:id>/<action>')
    @admin_required
    def source_action(id,action):
        if action not in ['toggle','rotate']:abort(400)
        token=None
        with db() as c:
            if not c.execute('SELECT 1 FROM sources WHERE id=?',(id,)).fetchone():abort(404)
            if action=='toggle':c.execute('UPDATE sources SET enabled=1-enabled WHERE id=?',(id,))
            else:
                token=secrets.token_urlsafe(40);c.execute('UPDATE sources SET token_hash=? WHERE id=?',(generate_password_hash(token),id))
            log(c,'Source '+str(id)+' '+action)
            sources=c.execute('SELECT id,name,enabled,created FROM sources').fetchall()
        return render_template('sources.html',sources=sources,token=token)

    @app.route('/admin/import',methods=['GET','POST'])
    @admin_required
    def admin_import():
        if request.method=='POST':
            try:
                with db() as c:r=c.execute('SELECT name FROM sources WHERE id=? AND enabled=1',(request.form.get('source_id'),)).fetchone()
                if not r:raise ValueError('Choose an enabled source')
                upload=request.files.get('file')
                rows=json.loads(upload.read() if upload else request.form.get('payload',''))
                count,n=import_rows(rows,r['name'])
                with db() as c:log(c,'Imported '+str(count)+' listings from '+r['name'])
                flash(f'Imported {count} listings; created {n} new match alerts.');return redirect(url_for('admin_import'))
            except (ValueError,TypeError,UnicodeError) as e:flash(str(e))
        with db() as c:sources=c.execute('SELECT id,name FROM sources WHERE enabled=1').fetchall()
        return render_template('import.html',sources=sources)

    @app.route('/admin/dealers',methods=['GET','POST'])
    @admin_required
    def admin_dealers():
        if request.method=='POST':
            with db() as c:
                r=c.execute("SELECT id FROM dealers WHERE id=? AND role='dealer'",(request.form.get('id'),)).fetchone()
                if not r:abort(400)
                c.execute('UPDATE dealers SET active=1-active WHERE id=?',(r['id'],));log(c,'Toggled dealer '+str(r['id']))
        with db() as c:dealers=c.execute('SELECT id,name,email,role,active FROM dealers ORDER BY id DESC').fetchall()
        return render_template('dealers.html',dealers=dealers)

    @app.get('/admin/audit')
    @admin_required
    def admin_audit():
        with db() as c:rows=c.execute('SELECT a.*,d.email FROM audit a LEFT JOIN dealers d ON d.id=a.actor ORDER BY a.id DESC LIMIT 200').fetchall()
        return render_template('audit.html',rows=rows)

    @app.route('/account',methods=['GET','POST'])
    @core.login_required
    def account():
        if request.method=='POST':
            with db() as c:
                d=c.execute('SELECT * FROM dealers WHERE id=?',(session['dealer'],)).fetchone()
                if not check_password_hash(d['password'],request.form.get('current_password','')):flash('Current password is incorrect.')
                elif len(request.form.get('password',''))<12:flash('New password must be at least 12 characters.')
                else:c.execute('UPDATE dealers SET password=? WHERE id=?',(generate_password_hash(request.form['password']),d['id']));flash('Password updated.')
        return render_template('account.html')
