from flask import Flask, request, jsonify, render_template, send_from_directory, send_file
from flask_cors import CORS
import sqlite3, os, uuid, base64, hashlib, io, secrets
from datetime import datetime, timedelta
from dotenv import load_dotenv
from werkzeug.middleware.dispatcher import DispatcherMiddleware


load_dotenv()

app = Flask(__name__)
app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {
    '/memorial': app
})

app.config['APPLICATION_ROOT'] = '/memorial'


app = Flask(
    __name__,
    static_url_path='/memorial/static',
    static_folder='static',
    template_folder='templates'
)

CORS(app)



BASE_DIR      = os.path.dirname(os.path.abspath(__file__))
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'static', 'uploads')
DB_PATH = '/home/collnetw/public_html/memorial/memorial.db'

ALLOWED_IMAGES = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
ALLOWED_DOCS   = {'pdf'}
CUTOFF_DATE    = datetime(2026, 7, 31, 23, 59, 59)
SESSION_HOURS  = 8   # longer session — no surprises during a work session

ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', 'ChangeMe123!')

app.config['UPLOAD_FOLDER']      = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 20 * 1024 * 1024

os.makedirs(UPLOAD_FOLDER, exist_ok=True)



# ── Database ──────────────────────────────────────────────────────────────────
def get_db():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn

def init_db():
    with get_db() as conn:
        conn.execute('''
            CREATE TABLE IF NOT EXISTS tributes (
                id            INTEGER PRIMARY KEY AUTOINCREMENT,
                tribute_ref   TEXT NOT NULL UNIQUE,
                name          TEXT NOT NULL,
                relationship  TEXT NOT NULL,
                message       TEXT,
                photo_path    TEXT,
                pdf_path      TEXT,
                submitted_at  TEXT NOT NULL
            )
        ''')
        # Sessions stored in DB — survives Flask restarts
        conn.execute('''
            CREATE TABLE IF NOT EXISTS admin_sessions (
                token      TEXT PRIMARY KEY,
                expires_at TEXT NOT NULL
            )
        ''')
        conn.execute('''
            CREATE TABLE IF NOT EXISTS gallery (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                filename     TEXT NOT NULL,
                caption      TEXT,
                uploader     TEXT,
                uploaded_at  TEXT NOT NULL
            )
        ''')
        try:
            conn.execute('ALTER TABLE gallery ADD COLUMN uploader TEXT')
        except Exception:
            pass
        for col in ['pdf_path TEXT', 'tribute_ref TEXT']:
            try:
                conn.execute(f'ALTER TABLE tributes ADD COLUMN {col}')
            except Exception:
                pass
        conn.commit()

def is_accepting():
    return datetime.now() <= CUTOFF_DATE

def allowed_file(filename, types):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in types

def generate_ref(name, submitted_at):
    initials = ''.join(w[0].upper() for w in name.split() if w)[:3]
    date_str = submitted_at[:10].replace('-', '')
    h = hashlib.sha256(f"{name}{submitted_at}".encode()).hexdigest()[:5].upper()
    return f"{initials}-{date_str}-{h}"

# ── Session helpers (DB-backed, survives restarts) ────────────────────────────
def create_session():
    token      = secrets.token_hex(32)
    expires_at = (datetime.now() + timedelta(hours=SESSION_HOURS)).isoformat()
    with get_db() as conn:
        # Clean old sessions first
        conn.execute("DELETE FROM admin_sessions WHERE expires_at < ?",
                     (datetime.now().isoformat(),))
        conn.execute("INSERT INTO admin_sessions (token, expires_at) VALUES (?,?)",
                     (token, expires_at))
        conn.commit()
    return token

def validate_session(token):
    if not token:
        return False
    with get_db() as conn:
        row = conn.execute(
            "SELECT expires_at FROM admin_sessions WHERE token=?", (token,)
        ).fetchone()
    if not row:
        return False
    if datetime.fromisoformat(row['expires_at']) < datetime.now():
        # Expired — remove it
        with get_db() as conn:
            conn.execute("DELETE FROM admin_sessions WHERE token=?", (token,))
            conn.commit()
        return False
    # Refresh expiry on activity
    new_expiry = (datetime.now() + timedelta(hours=SESSION_HOURS)).isoformat()
    with get_db() as conn:
        conn.execute("UPDATE admin_sessions SET expires_at=? WHERE token=?",
                     (new_expiry, token))
        conn.commit()
    return True

def delete_session(token):
    if token:
        with get_db() as conn:
            conn.execute("DELETE FROM admin_sessions WHERE token=?", (token,))
            conn.commit()

def get_token_from_request(req):
    auth = req.headers.get('Authorization', '')
    if auth.startswith('Bearer '):
        return auth[7:].strip()
    return None

def is_admin(req):
    return validate_session(get_token_from_request(req))

def admin_required(f):
    from functools import wraps
    @wraps(f)
    def decorated(*args, **kwargs):
        if not is_admin(request):
            return jsonify({'error': 'Admin authentication required.'}), 401
        return f(*args, **kwargs)
    return decorated

# ── Public routes ─────────────────────────────────────────────────────────────
@app.route('/')
def index():
    return render_template('index.html')

@app.route('/api/tributes', methods=['GET'])
def get_tributes():
    with get_db() as conn:
        rows = conn.execute(
            'SELECT * FROM tributes ORDER BY submitted_at DESC'
        ).fetchall()
    all_rows = [dict(r) for r in rows]
    # Admins get full data including tribute_ref; public gets sanitised view
    if is_admin(request):
        return jsonify(all_rows)
    safe = [{
        'id':           r['id'],
        'name':         r['name'],
        'relationship': r['relationship'],
        'message':      r['message'],
        'photo_path':   r['photo_path'],
        'pdf_path':     r['pdf_path'],
        'submitted_at': r['submitted_at'],
    } for r in all_rows]
    return jsonify(safe)

@app.route('/api/tributes', methods=['POST'])
def submit_tribute():
    if not is_accepting():
        return jsonify({'error': 'Tribute submissions closed on July 31st.'}), 403

    name         = request.form.get('name', '').strip()
    relationship = request.form.get('relationship', '').strip()
    message      = request.form.get('message', '').strip()

    if not name or not relationship:
        return jsonify({'error': 'Name and relationship are required.'}), 400

    photo_path = None
    pdf_path   = None

    if 'photo' in request.files:
        f = request.files['photo']
        if f and f.filename and allowed_file(f.filename, ALLOWED_IMAGES):
            ext = f.filename.rsplit('.', 1)[1].lower()
            fname = f"{uuid.uuid4().hex}.{ext}"
            f.save(os.path.join(UPLOAD_FOLDER, fname))
            photo_path = fname

    if 'pdf_tribute' in request.files:
        pf = request.files['pdf_tribute']
        if pf and pf.filename and allowed_file(pf.filename, ALLOWED_DOCS):
            fname = f"{uuid.uuid4().hex}.pdf"
            pf.save(os.path.join(UPLOAD_FOLDER, fname))
            pdf_path = fname

    if not message and not pdf_path:
        return jsonify({'error': 'Please write a tribute or upload a PDF.'}), 400

    submitted_at = datetime.now().isoformat()
    tribute_ref  = generate_ref(name, submitted_at)

    with get_db() as conn:
        conn.execute(
            'INSERT INTO tributes (tribute_ref, name, relationship, message, '
            'photo_path, pdf_path, submitted_at) VALUES (?,?,?,?,?,?,?)',
            (tribute_ref, name, relationship, message, photo_path, pdf_path, submitted_at)
        )
        conn.commit()

    return jsonify({'success': True, 'tribute_ref': tribute_ref})

@app.route('/api/status')
def status():
    return jsonify({'accepting': is_accepting(), 'cutoff': CUTOFF_DATE.strftime('%B %d, %Y')})

@app.route('/static/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(UPLOAD_FOLDER, filename)

# ── Admin: auth ───────────────────────────────────────────────────────────────
@app.route('/api/admin/login', methods=['POST'])
def admin_login():
    data     = request.get_json(silent=True) or {}
    password = data.get('password', '').strip()
    if not secrets.compare_digest(password, ADMIN_PASSWORD):
        return jsonify({'error': 'Invalid password.'}), 401
    token = create_session()
    return jsonify({'token': token, 'expires_in': f'{SESSION_HOURS} hours'})

@app.route('/api/admin/logout', methods=['POST'])
def admin_logout():
    delete_session(get_token_from_request(request))
    return jsonify({'success': True})

@app.route('/api/admin/verify', methods=['GET'])
def admin_verify():
    return jsonify({'valid': is_admin(request)})

# ── Admin: image base64 ───────────────────────────────────────────────────────
@app.route('/api/image_b64/<filename>')
@admin_required
def image_b64(filename):
    filepath = os.path.join(UPLOAD_FOLDER, filename)
    if not os.path.exists(filepath):
        return jsonify({'error': 'not found'}), 404
    ext  = filename.rsplit('.', 1)[-1].lower()
    mime = {'jpg':'image/jpeg','jpeg':'image/jpeg','png':'image/png',
            'gif':'image/gif','webp':'image/webp'}.get(ext, 'image/jpeg')
    with open(filepath, 'rb') as f:
        data = base64.b64encode(f.read()).decode('utf-8')
    return jsonify({'data_uri': f'data:{mime};base64,{data}'})

# ── Admin: DOCX download ──────────────────────────────────────────────────────
@app.route('/api/tribute_docx/<int:tribute_id>')
@admin_required
def tribute_docx(tribute_id):
    with get_db() as conn:
        row = conn.execute('SELECT * FROM tributes WHERE id=?', (tribute_id,)).fetchone()
    if not row:
        return jsonify({'error': 'Not found'}), 404

    t = dict(row)
    edited_msg = request.args.get('edited_msg')
    if edited_msg:
        t['message'] = edited_msg

    dt         = datetime.fromisoformat(t['submitted_at'])
    clean_date = dt.strftime('%-d %B %Y') if os.name != 'nt' else dt.strftime('%d %B %Y').lstrip('0')

    from docx import Document as DocxDoc
    from docx.shared import Pt, Inches, RGBColor
    from docx.enum.text import WD_ALIGN_PARAGRAPH

    doc = DocxDoc()
    for section in doc.sections:
        section.top_margin    = Inches(1)
        section.bottom_margin = Inches(1)
        section.left_margin   = Inches(1.2)
        section.right_margin  = Inches(1.2)

    hdr = doc.add_paragraph()
    hdr.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run = hdr.add_run('In Loving Memory of Lady Monica Egowure Onyegbado')
    run.bold = True; run.font.size = Pt(13)
    run.font.color.rgb = RGBColor(0x3b, 0x1a, 0x5a)

    sub = doc.add_paragraph()
    sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
    sub.add_run('1937 — 2026').font.size = Pt(10)
    doc.add_paragraph()

    name_p   = doc.add_paragraph()
    name_run = name_p.add_run(t['name'])
    name_run.bold = True; name_run.font.size = Pt(14)
    name_run.font.color.rgb = RGBColor(0x3b, 0x1a, 0x5a)

    meta_p = doc.add_paragraph()
    meta_p.add_run(f"{t['relationship']}  ·  {clean_date}").font.size = Pt(10)
    meta_p.runs[0].font.color.rgb = RGBColor(0x7a, 0x54, 0x90)
    doc.add_paragraph()

    if t.get('message'):
        msg_p   = doc.add_paragraph()
        msg_run = msg_p.add_run(t['message'])
        msg_run.font.size = Pt(12); msg_run.italic = True

    if t.get('photo_path'):
        img_path = os.path.join(UPLOAD_FOLDER, t['photo_path'])
        if os.path.exists(img_path):
            doc.add_paragraph()
            doc.add_picture(img_path, width=Inches(2.5))

    if t.get('pdf_path'):
        doc.add_paragraph()
        note = doc.add_paragraph()
        note.add_run('A PDF tribute was also submitted alongside this text tribute.').italic = True
        note.runs[0].font.size = Pt(9)

    buf = io.BytesIO()
    doc.save(buf); buf.seek(0)

    safe_name = t['name'].replace(' ', '_').replace('/', '_')
    return send_file(buf, as_attachment=True,
                     download_name=f"Tribute_{safe_name}.docx",
                     mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document')


# ── Gallery routes ────────────────────────────────────────────────────────────
@app.route('/api/gallery', methods=['GET'])
def get_gallery():
    with get_db() as conn:
        rows = conn.execute('SELECT * FROM gallery ORDER BY uploaded_at DESC').fetchall()
    return jsonify([dict(r) for r in rows])

@app.route('/api/gallery', methods=['POST'])
def upload_gallery():
    """Public — anyone can upload gallery photos. Admin can delete them."""
    if 'photo' not in request.files:
        return jsonify({'error': 'No file provided'}), 400
    f = request.files['photo']
    if not f or not f.filename or not allowed_file(f.filename, ALLOWED_IMAGES):
        return jsonify({'error': 'Invalid image file'}), 400
    ext   = f.filename.rsplit('.', 1)[1].lower()
    fname = f"gallery_{uuid.uuid4().hex}.{ext}"
    f.save(os.path.join(UPLOAD_FOLDER, fname))
    caption     = request.form.get('caption', '').strip()
    uploader    = request.form.get('uploader', '').strip()
    uploaded_at = datetime.now().isoformat()
    with get_db() as conn:
        conn.execute('INSERT INTO gallery (filename, caption, uploader, uploaded_at) VALUES (?,?,?,?)',
                     (fname, caption, uploader, uploaded_at))
        conn.commit()
    return jsonify({'success': True, 'filename': fname})

@app.route('/api/gallery/<int:photo_id>', methods=['DELETE'])
@admin_required
def delete_gallery(photo_id):
    with get_db() as conn:
        row = conn.execute('SELECT filename FROM gallery WHERE id=?', (photo_id,)).fetchone()
    if not row:
        return jsonify({'error': 'Not found'}), 404
    filepath = os.path.join(UPLOAD_FOLDER, row['filename'])
    if os.path.exists(filepath):
        os.remove(filepath)
    with get_db() as conn:
        conn.execute('DELETE FROM gallery WHERE id=?', (photo_id,))
        conn.commit()
    return jsonify({'success': True})

with app.app_context():
    init_db()

if __name__ == '__main__':
    app.run(debug=True, port=5000)