import os, json, re
from datetime import datetime, timezone
from functools import wraps
from flask import Flask, render_template, request, redirect, url_for, session, flash, jsonify
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import check_password_hash, generate_password_hash
from dotenv import load_dotenv
load_dotenv()
app=Flask(__name__)
app.config['SECRET_KEY']=os.environ.get('SECRET_KEY','CHANGE-THIS-BEFORE-DEPLOYMENT')
app.config['SQLALCHEMY_DATABASE_URI']=os.environ.get('DATABASE_URL','sqlite:///mejoric.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS']=False
app.config['SESSION_COOKIE_HTTPONLY']=True
app.config['SESSION_COOKIE_SAMESITE']='Lax'
app.config['SESSION_COOKIE_SECURE']=os.environ.get('COOKIE_SECURE','0')=='1'
app.config['MAX_CONTENT_LENGTH']=128*1024
db=SQLAlchemy(app)
class Admin(db.Model):
 id=db.Column(db.Integer,primary_key=True)
 username=db.Column(db.String(100),unique=True,nullable=False)
 password_hash=db.Column(db.String(255),nullable=False)
class Lead(db.Model):
 id=db.Column(db.Integer,primary_key=True)
 created_at=db.Column(db.DateTime,default=lambda:datetime.now(timezone.utc),nullable=False)
 role=db.Column(db.String(40),nullable=False)
 name=db.Column(db.String(150),nullable=False)
 email=db.Column(db.String(180),nullable=False)
 mobile=db.Column(db.String(25),nullable=False)
 city=db.Column(db.String(120),nullable=False)
 school=db.Column(db.String(180))
 class_level=db.Column(db.String(15))
 school_role=db.Column(db.String(120))
 interests=db.Column(db.Text)
 topics=db.Column(db.Text)
 suggested_topic=db.Column(db.Text)
 preferred_time=db.Column(db.String(40))
 guardian_name=db.Column(db.String(150))
 guardian_email=db.Column(db.String(180))
 guardian_mobile=db.Column(db.String(25))
 consent=db.Column(db.Boolean,nullable=False)
 guardian_consent=db.Column(db.Boolean,default=False)
 status=db.Column(db.String(30),default='New',nullable=False)
 notes=db.Column(db.Text)
 updated_at=db.Column(db.DateTime,default=lambda:datetime.now(timezone.utc),onupdate=lambda:datetime.now(timezone.utc))
COUNTRY_CODES={'+91':(10,10),'+1':(10,10),'+44':(9,10),'+61':(9,9),'+971':(9,9),'+65':(8,8),'+49':(7,13),'+33':(9,9),'+81':(9,10),'+966':(9,9),'+974':(8,8),'+977':(10,10),'+880':(10,10)}
EMAIL_PATTERN=r'[A-Za-z0-9.!#$%&\'*+/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+'
def valid_email(value):return bool(len(value)<=180 and re.fullmatch(EMAIL_PATTERN,value,flags=re.ASCII))
def validate_phone(code,number):
 if code not in COUNTRY_CODES or not re.fullmatch(r'[0-9]+',number):return False
 low,high=COUNTRY_CODES[code]
 return low<=len(number)<=high and (code!='+91' or number[0] in '6789')
ROLES=['Student','Parent','Teacher','Counsellor','School leader','Other']
INTERESTS=['1-on-1 conversation','Group workshops','Check-in or career assessment','Teacher / staff training','Upcoming session on 5 Nov','Upcoming session on 9 Nov']
STATUSES=['New','Contacted','Paid','Not interested']
TOPICS={'1-6':['Making New Friends','Handling Big Feelings','Focus & Study Habits','Supporting Big Feelings at Home','Study Routines Without the Nagging'], '7-8':['Know Your Strengths','World of Careers','How Your Subjects Connect to Careers','Talking About Careers Without Pressure','Reading Your Child’s Report Card Differently'], '9-10':['Career Clusters Explained','Choosing Your Stream','Talking to Family About Your Choices','Navigating the Stream Decision as a Family','Managing Exam Anxiety at Home'], '11-12':['Balancing Boards and Entrance Prep','Building Your Application Story','Life After School','Supporting Through Entrance Exam Season','Letting Go: The Application Conversation'], 'staff':['Recognising Student Distress Early','Spotting Learning Difficulties Early','Career Conversations with Parents','Facilitating Classroom Check-ins','Leadership training']}
@app.context_processor
def inject():return dict(topics=TOPICS,interests=INTERESTS,roles=ROLES,statuses=STATUSES)
@app.after_request
def security_headers(r):
 r.headers['X-Content-Type-Options']='nosniff';r.headers['X-Frame-Options']='SAMEORIGIN';r.headers['Referrer-Policy']='strict-origin-when-cross-origin';return r
@app.get('/')
def home():return render_template('index.html')
@app.post('/register')
def register():
 f=request.form
 role=f.get('role','');name=f.get('name','').strip();email=f.get('email','').strip();mobile_number=f.get('mobile','').strip();mobile_code=f.get('mobile_code','').strip();mobile=f'{mobile_code}{mobile_number}';city=f.get('city','').strip()
 interests=f.getlist('interests');topics=f.getlist('topics');level=f.get('class_level','').strip();guardian_name=f.get('guardian_name','').strip();guardian_email=f.get('guardian_email','').strip();guardian_number=f.get('guardian_mobile','').strip();guardian_code=f.get('guardian_mobile_code','').strip();guardian_mobile=f'{guardian_code}{guardian_number}'
 errors=[]
 if role not in ROLES:errors.append('Choose a valid role.')
 if not all([name,email,mobile,city]):errors.append('Complete all required fields.')
 if not valid_email(email):errors.append('Enter a valid email address.')
 if not validate_phone(mobile_code,mobile_number):errors.append('Select a valid country code and enter a valid mobile number (digits only).')
 if not interests or any(x not in INTERESTS for x in interests):errors.append('Select at least one valid interest.')
 if role in ['Student','Parent'] and level not in [str(i) for i in range(1,13)]:errors.append('Select a class.')
 if role=='Student' and (not guardian_name or not guardian_email or not guardian_mobile or not valid_email(guardian_email) or not validate_phone(guardian_code,guardian_number)):errors.append('Student registrations require valid parent/guardian contact details.')
 if f.get('consent')!='yes':errors.append('Consent is required.')
 if role=='Student' and f.get('guardian_consent')!='yes':errors.append('Parent/guardian agreement is required.')
 if errors:
  for e in errors:flash(e,'error')
  return redirect(url_for('home')+'#register')
 lead=Lead(role=role,name=name[:150],email=email[:180],mobile=mobile[:25],city=city[:120],school=f.get('school','').strip()[:180],class_level=level if role in ['Student','Parent'] else None,school_role=f.get('school_role','').strip()[:120] if role not in ['Student','Parent'] else None,interests=json.dumps(interests),topics=json.dumps(topics[:30]),suggested_topic=f.get('suggested_topic','').strip()[:2000],preferred_time=f.get('preferred_time','')[:40],guardian_name=guardian_name[:150] if role=='Student' else None,guardian_email=guardian_email[:180] if role=='Student' else None,guardian_mobile=guardian_mobile[:25] if role=='Student' else None,consent=True,guardian_consent=role=='Student' and f.get('guardian_consent')=='yes')
 db.session.add(lead);db.session.commit()
 return redirect(url_for('thanks'))
@app.get('/thank-you')
def thanks():return render_template('thanks.html')
def admin_required(fn):
 @wraps(fn)
 def wrapped(*args,**kwargs):
  if not session.get('admin_id'):return redirect(url_for('login'))
  return fn(*args,**kwargs)
 return wrapped
@app.route('/admin/login',methods=['GET','POST'])
def login():
 if request.method=='POST':
  user=Admin.query.filter_by(username=request.form.get('username','')).first()
  if user and check_password_hash(user.password_hash,request.form.get('password','')):
   session.clear();session['admin_id']=user.id;return redirect(url_for('dashboard'))
  flash('Invalid credentials.','error')
 return render_template('login.html')
@app.post('/admin/logout')
@admin_required
def logout():session.clear();return redirect(url_for('login'))
@app.get('/admin')
@admin_required
def dashboard():
 status=request.args.get('status','');q=request.args.get('q','').strip();query=Lead.query
 if status in STATUSES:query=query.filter_by(status=status)
 if q:query=query.filter(db.or_(Lead.name.ilike('%'+q+'%'),Lead.email.ilike('%'+q+'%'),Lead.mobile.ilike('%'+q+'%'),Lead.school.ilike('%'+q+'%')))
 leads=query.order_by(Lead.created_at.desc()).limit(1000).all()
 counts={s:Lead.query.filter_by(status=s).count() for s in STATUSES}
 return render_template('admin.html',leads=leads,counts=counts,status=status,q=q)
@app.post('/admin/leads/<int:lead_id>')
@admin_required
def update_lead(lead_id):
 lead=db.get_or_404(Lead,lead_id);status=request.form.get('status','')
 if status not in STATUSES:flash('Invalid status.','error');return redirect(url_for('dashboard'))
 lead.status=status;lead.notes=request.form.get('notes','')[:3000];db.session.commit();flash('Lead updated.','success');return redirect(url_for('dashboard'))
@app.cli.command('init-db')
def init_db():db.create_all();print('Database ready.')
@app.cli.command('create-admin')
def create_admin():
 import getpass
 username=input('Admin username: ').strip();password=getpass.getpass('Admin password: ')
 if len(password)<12:raise ValueError('Use at least 12 characters.')
 user=Admin.query.filter_by(username=username).first()
 if not user:user=Admin(username=username);db.session.add(user)
 user.password_hash = generate_password_hash(password, method="pbkdf2:sha256:600000")
 db.session.commit()
 print("Admin saved.")
if __name__=='__main__':app.run(port=5095, debug=os.environ.get('FLASK_DEBUG')=='1')
