import os, sqlite3, secrets, json, math, re
from pathlib import Path
from functools import wraps
from urllib.parse import urlparse
from flask import Flask, request, session, redirect, url_for, render_template, flash, abort, jsonify
from werkzeug.security import generate_password_hash, check_password_hash
ROOT=Path(__file__).resolve().parent
DATA=Path(os.getenv('DATA_DIR',ROOT/'data')); DATA.mkdir(parents=True,exist_ok=True)
DB=DATA/'dealers.sqlite3'
key=os.getenv('SECRET_KEY')
if not key:
    keyfile=DATA/'session.key'
    if not keyfile.exists():
        keyfile.write_text(secrets.token_hex(32)); keyfile.chmod(0o600)
    key=keyfile.read_text()
app=Flask(__name__)
app.config.update(SECRET_KEY=key,MAX_CONTENT_LENGTH=2*1024*1024,SESSION_COOKIE_NAME='oasis_dealer',SESSION_COOKIE_HTTPONLY=True,SESSION_COOKIE_SAMESITE='Lax',SESSION_COOKIE_SECURE=os.getenv('COOKIE_SECURE','0')=='1')
def db():
    c=sqlite3.connect(DB); c.row_factory=sqlite3.Row; c.execute('PRAGMA foreign_keys=ON'); return c
with db() as c:
    c.executescript('''CREATE TABLE IF NOT EXISTS dealers(id INTEGER PRIMARY KEY,name TEXT NOT NULL,email TEXT UNIQUE NOT NULL,password TEXT NOT NULL);
    CREATE TABLE IF NOT EXISTS vehicles(id INTEGER PRIMARY KEY,source_id TEXT UNIQUE NOT NULL,make TEXT,model TEXT,year INTEGER,price INTEGER,mileage INTEGER,city TEXT,lat REAL,lon REAL,title TEXT,seller TEXT,url TEXT,source TEXT,created TEXT DEFAULT CURRENT_TIMESTAMP);
    CREATE TABLE IF NOT EXISTS interests(id INTEGER PRIMARY KEY,dealer_id INTEGER REFERENCES dealers(id),name TEXT,make TEXT,model TEXT,min_year INTEGER,max_price INTEGER,max_mileage INTEGER,lat REAL,lon REAL,radius INTEGER);
    CREATE TABLE IF NOT EXISTS leads(dealer_id INTEGER REFERENCES dealers(id),vehicle_id INTEGER REFERENCES vehicles(id),status TEXT DEFAULT 'Saved',notes TEXT DEFAULT '',PRIMARY KEY(dealer_id,vehicle_id));
    CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY,dealer_id INTEGER REFERENCES dealers(id),interest_id INTEGER REFERENCES interests(id) ON DELETE CASCADE,vehicle_id INTEGER REFERENCES vehicles(id),seen INTEGER DEFAULT 0,created TEXT DEFAULT CURRENT_TIMESTAMP,UNIQUE(interest_id,vehicle_id));''')
with db() as c:
    c.execute("CREATE TABLE IF NOT EXISTS connections(dealer_id INTEGER PRIMARY KEY REFERENCES dealers(id),page_id TEXT DEFAULT '',page_url TEXT DEFAULT '',city TEXT DEFAULT '',lat REAL DEFAULT 36.16,lon REAL DEFAULT -86.78,radius INTEGER DEFAULT 100)")

@app.route('/connections',methods=['GET','POST'])
def connections():
    if not session.get('dealer'): return redirect(url_for('login'))
    if request.method=='POST':
        try:
            f=request.form; page_id=f.get('page_id','').strip(); page_url=f.get('page_url','').strip()
            if page_id and not page_id.isdigit(): raise ValueError('Page ID must be numeric')
            if page_url and (urlparse(page_url).scheme!='https' or urlparse(page_url).hostname not in ['facebook.com','www.facebook.com']): raise ValueError('Enter an HTTPS Facebook Page URL')
            with db() as c:
                c.execute('INSERT INTO connections(dealer_id,page_id,page_url,city,lat,lon,radius) VALUES(?,?,?,?,?,?,?) ON CONFLICT(dealer_id) DO UPDATE SET page_id=excluded.page_id,page_url=excluded.page_url,city=excluded.city,lat=excluded.lat,lon=excluded.lon,radius=excluded.radius',(session['dealer'],page_id,page_url,f.get('city','')[:100],num(f,'lat',float(setting('default_lat')),-90,90),num(f,'lon',float(setting('default_lon')),-180,180),int(num(f,'radius',int(setting('default_radius')),1,int(setting('max_radius'))))))
            flash('Location and Page references saved. Use Connect Facebook Page to authorize Facebook.')
        except (ValueError,TypeError) as e: flash(str(e))
    with db() as c: connection=c.execute('SELECT * FROM connections WHERE dealer_id=?',(session['dealer'],)).fetchone()
    return render_template('connections.html',connection=connection)

def login_required(f):
    @wraps(f)
    def wrapped(*a,**kw):
        if not session.get('dealer'): return redirect(url_for('login'))
        return f(*a,**kw)
    return wrapped
@app.before_request
def csrf():
    if session.get('dealer'):
        with db() as c: account=c.execute('SELECT active FROM dealers WHERE id=?',(session['dealer'],)).fetchone()
        if not account or not account['active']:
            session.clear(); return redirect(url_for('login'))
    if request.path in ['/api/listings','/facebook/deauthorize','/facebook/delete-data']: return
    if 'csrf' not in session: session['csrf']=secrets.token_hex(24)
    if request.method=='POST' and not secrets.compare_digest(session['csrf'],request.form.get('csrf','')): abort(400)
@app.context_processor
def context():
    return {'csrf':session.get('csrf'),'dealer':session.get('name'),'settings':all_settings(),'statuses':lead_statuses(),'is_admin':is_admin()}

def num(data,key,default=0,minval=0,maxval=100000000):
    n=float(data.get(key) or default)
    if not math.isfinite(n) or not minval<=n<=maxval: raise ValueError(key+' is out of range')
    return n
def distance(a,b,x,y):
    p=math.pi/180; h=math.sin((x-a)*p/2)**2+math.cos(a*p)*math.cos(x*p)*math.sin((y-b)*p/2)**2
    return 3958.8*2*math.asin(min(1,math.sqrt(h)))
def matches(v,i):
    if i['make'] and i['make'].lower()!=v['make'].lower(): return False
    if i['model'] and i['model'].lower() not in v['model'].lower(): return False
    if v['year']<i['min_year'] or v['price']>i['max_price'] or v['mileage']>i['max_mileage']: return False
    return distance(i['lat'],i['lon'],v['lat'],v['lon'])<=i['radius']
def process_matches(c):
    count=0
    for i in 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').fetchall():
        for v in c.execute('SELECT * FROM vehicles').fetchall():
            if matches(v,i):
                count+=c.execute('INSERT OR IGNORE INTO alerts(dealer_id,interest_id,vehicle_id) VALUES(?,?,?)',(i['dealer_id'],i['id'],v['id'])).rowcount
    return count
@app.route('/')
def home(): return redirect(url_for('dashboard')) if session.get('dealer') else render_template('landing.html')
@app.route('/register',methods=['GET','POST'])
def register():
    if setting('registration_open')!='1':
        flash('Registration is currently closed.'); return redirect(url_for('login'))
    if request.method=='POST':
        name=request.form.get('name','').strip(); email=request.form.get('email','').strip().lower(); pw=request.form.get('password','')
        if not name or not re.fullmatch(r'[^\s@]+@[^\s@]+\.[^\s@]+',email) or len(pw)<12:
            flash('Enter dealership name, valid email, and a password of at least 12 characters.')
        else:
            try:
                with db() as c: c.execute('INSERT INTO dealers(name,email,password) VALUES(?,?,?)',(name,email,generate_password_hash(pw)))
                flash('Account created. Sign in to start sourcing.'); return redirect(url_for('login'))
            except sqlite3.IntegrityError: flash('This email is already registered.')
    return render_template('auth.html',register=True)
@app.route('/login',methods=['GET','POST'])
def login():
    if request.method=='POST':
        with db() as c: d=c.execute('SELECT * FROM dealers WHERE email=?',(request.form.get('email','').lower().strip(),)).fetchone()
        if d and d['active'] and check_password_hash(d['password'],request.form.get('password','')):
            session.clear(); session.update(dealer=d['id'],name=d['name'],csrf=secrets.token_hex(24)); return redirect(url_for('dashboard'))
        flash('Email or password is incorrect.')
    return render_template('auth.html',register=False)
@app.post('/logout')
def logout(): session.clear(); return redirect(url_for('home'))
@app.get('/dashboard')
@login_required
def dashboard():
    with db() as c:
        stats=[c.execute('SELECT COUNT(*) FROM interests WHERE dealer_id=?',(session['dealer'],)).fetchone()[0],c.execute('SELECT COUNT(*) FROM alerts WHERE dealer_id=? AND seen=0',(session['dealer'],)).fetchone()[0],c.execute('SELECT COUNT(*) FROM leads WHERE dealer_id=?',(session['dealer'],)).fetchone()[0]]
        alerts=c.execute('SELECT a.id alert_id,v.*,i.name interest_name FROM alerts a JOIN vehicles v ON v.id=a.vehicle_id JOIN interests i ON i.id=a.interest_id WHERE a.dealer_id=? ORDER BY a.id DESC LIMIT 30',(session['dealer'],)).fetchall()
    return render_template('dashboard.html',stats=stats,vehicles=alerts)
@app.get('/search')
@login_required
def search():
    q=request.args; sql='SELECT * FROM vehicles WHERE 1=1'; args=[]
    for field in ('make','model','city'):
        if q.get(field): sql+=' AND '+field+' LIKE ?'; args.append('%'+q[field]+'%')
    try:
        for field,op in [('price','<='),('mileage','<='),('year','>=')]:
            if q.get(field): sql+=' AND '+field+op+'?'; args.append(num(q,field))
        with db() as c: vehicles=c.execute(sql+' ORDER BY created DESC,id DESC LIMIT 200',args).fetchall()
    except ValueError: abort(400)
    return render_template('search.html',vehicles=vehicles)
@app.route('/interests',methods=['GET','POST'])
@login_required
def interests():
    if request.method=='POST':
        try:
            f=request.form
            if not f.get('name','').strip(): raise ValueError('Give your interest a name')
            vals=(session['dealer'],f['name'][:100],f.get('make','').strip(),f.get('model','').strip(),int(num(f,'min_year',2000,1900,2100)),int(num(f,'max_price',50000)),int(num(f,'max_mileage',100000)),num(f,'lat',float(setting('default_lat')),-90,90),num(f,'lon',float(setting('default_lon')),-180,180),int(num(f,'radius',int(setting('default_radius')),1,int(setting('max_radius')))))
            with db() as c:
                if c.execute('SELECT COUNT(*) FROM interests WHERE dealer_id=?',(session['dealer'],)).fetchone()[0]>=int(setting('max_interests')): raise ValueError('Active interest limit reached')
                c.execute('INSERT INTO interests(dealer_id,name,make,model,min_year,max_price,max_mileage,lat,lon,radius) VALUES(?,?,?,?,?,?,?,?,?,?)',vals); process_matches(c)
            flash('Interest saved. New imported listings will be checked automatically.'); return redirect(url_for('interests'))
        except (ValueError,TypeError) as e: flash(str(e))
    with db() as c: rows=c.execute('SELECT * FROM interests WHERE dealer_id=?',(session['dealer'],)).fetchall()
    with db() as c: defaults=c.execute('SELECT * FROM connections WHERE dealer_id=?',(session['dealer'],)).fetchone()
    return render_template('interests.html',interests=rows,defaults=defaults)
@app.post('/interests/<int:id>/delete')
@login_required
def delete_interest(id):
    with db() as c: c.execute('DELETE FROM interests WHERE id=? AND dealer_id=?',(id,session['dealer']))
    return redirect(url_for('interests'))
@app.get('/vehicles/<int:id>')
@login_required
def vehicle(id):
    with db() as c:
        v=c.execute('SELECT * FROM vehicles WHERE id=?',(id,)).fetchone(); lead=c.execute('SELECT * FROM leads WHERE dealer_id=? AND vehicle_id=?',(session['dealer'],id)).fetchone()
        c.execute('UPDATE alerts SET seen=1 WHERE dealer_id=? AND vehicle_id=?',(session['dealer'],id))
    if not v: abort(404)
    return render_template('vehicle.html',v=v,lead=lead)
@app.post('/vehicles/<int:id>/save')
@login_required
def save(id):
    status=request.form.get('status',lead_statuses()[0])
    if status not in lead_statuses(): abort(400)
    with db() as c:
        if not c.execute('SELECT 1 FROM vehicles WHERE id=?',(id,)).fetchone(): abort(404)
        c.execute('INSERT INTO leads(dealer_id,vehicle_id,status,notes) VALUES(?,?,?,?) ON CONFLICT(dealer_id,vehicle_id) DO UPDATE SET status=excluded.status,notes=excluded.notes',(session['dealer'],id,status,request.form.get('notes','')[:5000]))
    flash('Lead updated.'); return redirect(url_for('vehicle',id=id))
@app.get('/leads')
@login_required
def leads():
    with db() as c: rows=c.execute('SELECT v.*,l.status FROM leads l JOIN vehicles v ON v.id=l.vehicle_id WHERE l.dealer_id=? ORDER BY v.id DESC',(session['dealer'],)).fetchall()
    return render_template('leads.html',vehicles=rows)
@app.post('/api/listings')
def ingest():
    credential=request.headers.get('Authorization','').removeprefix('Bearer ')
    source=authorized_source(credential)
    if not source: abort(401)
    try:
        count,n=import_rows(request.get_json(silent=True),source)
        return jsonify(processed=count,new_alerts=n)
    except (ValueError,TypeError) as e: return jsonify(error=str(e)),400

@app.get('/health')
def health(): return {'status':'ok'}
@app.after_request
def headers(response):
    response.headers['X-Content-Type-Options']='nosniff'; response.headers['X-Frame-Options']='DENY'; response.headers['Referrer-Policy']='strict-origin-when-cross-origin'; response.headers['Cache-Control']='no-store'
    return response
from admin import install
install(app)
from integrations import install as install_integrations
install_integrations(app)

if __name__=='__main__': app.run(host='127.0.0.1',port=int(os.getenv('PORT','5001')))
