"""
mcq_ppt_generator.py — EduRuby Enhanced (final)
-----------------------------------------------
Creates branded PowerPoint slides for MCQ questions:
- Question + options on first slide
- Explanation + generated image on second slide
- If question had an image extracted from PDF, show it on first slide
- Smart retry + fallback image generation (OpenAI → Gemini)
- Theme and logo automatically applied
"""

import os
import io
import uuid
import json
import base64
import shutil
import subprocess
import re
from pathlib import Path
from typing import Optional, Tuple
from dotenv import load_dotenv
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
from pptx.enum.text import MSO_ANCHOR
from PIL import Image
import requests
import google.generativeai as genai
from celery_app import celery
import matplotlib.pyplot as plt
import textwrap

from celery_app import celery
#from MCQ_creator import IMAGES_DIR


# -------------------------------
# CONFIG
# -------------------------------
load_dotenv()

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")

BASE_DIR = Path(__file__).parent
# OUTPUTS_DIR = BASE_DIR / "outputs"
# IMAGES_DIR = OUTPUTS_DIR / "ppt_images"
# PPT_PATH = OUTPUTS_DIR / "mcq_presentation.pptx"
# PDF_PATH = OUTPUTS_DIR / "mcq_presentation.pdf"
# JSON_PATH = OUTPUTS_DIR / "mcq_data.json"

# os.makedirs(IMAGES_DIR, exist_ok=True)

# -------------------------------
# THEME CONFIG
# -------------------------------
COMPANY_LOGO_PATH = "logo.png"
THEME_BG = RGBColor(238, 231, 255)
THEME_TEXT = RGBColor(45, 20, 85)
THEME_TEXT_SOFT = RGBColor(80, 50, 120)

# -------------------------------
# IMAGE GENERATION HELPERS
# -------------------------------
def generate_image_openai(prompt: str, out_dir: str) -> Tuple[Optional[str], Optional[str]]:
    """Generate an educational image using OpenAI DALL·E."""
    if not OPENAI_API_KEY:
        return None, "Missing OPENAI_API_KEY"
    try:
        print(f"🎨 Generating image with OpenAI → {prompt[:60]}...")
        r = requests.post(
            "https://api.openai.com/v1/images/generations",
            headers={
                "Authorization": f"Bearer {OPENAI_API_KEY}",
                "Content-Type": "application/json"
            },
            json={
                "model": "dall-e-3",
                "prompt": prompt,
                "size": "1024x1024",
                "response_format": "b64_json"
            },
            timeout=90
        )
        if r.status_code != 200:
            return None, f"{r.status_code} {r.text}"

        b64 = r.json()["data"][0]["b64_json"]
        img = Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB")
        fname = f"openai_{uuid.uuid4().hex[:8]}.jpg"
        path = os.path.join(out_dir, fname)
        img.save(path, "JPEG", quality=92)
        print(f"✅ OpenAI image saved: {path}")
        return path, None
    except Exception as e:
        return None, f"OpenAI error: {e}"


def generate_image_gemini(prompt: str, out_dir: str) -> Tuple[Optional[str], Optional[str]]:
    """Fallback image generation using Gemini."""
    if not GEMINI_API_KEY:
        return None, "Missing GEMINI_API_KEY"
    try:
        genai.configure(api_key=GEMINI_API_KEY)
        response = genai.images.generate(
            model="models/imagegeneration",
            prompt=f"Educational illustration: {prompt}. Flat, clean vector style, no text or watermark."
        )
        if not getattr(response, "generated_images", None):
            return None, "No image data from Gemini"

        raw = base64.b64decode(response.generated_images[0])
        with Image.open(io.BytesIO(raw)) as im:
            fname = f"gemini_{uuid.uuid4().hex[:8]}.jpg"
            path = os.path.join(out_dir, fname)
            im.convert("RGB").save(path, "JPEG", quality=92)
            print(f"✅ Gemini image saved: {path}")
            return path, None
    except Exception as e:
        return None, f"Gemini error: {e}"


def simplify_visual_prompt(prompt: str) -> str:
    """Simplify long logic-heavy question into a short, image-friendly prompt."""
    keep = []
    for token in prompt.split():
        if token.lower() in {"north", "south", "east", "west", "left", "right"}:
            keep.append(token)
    if keep:
        base = " ".join(keep)
        return f"Educational diagram showing directional turns ({base})"
    return "Simple educational illustration showing the main concept."

def detect_latex(s: str) -> bool:
    if not s:
        return False
    keys = ["\\(", "\\)", "\\[", "\\]", "$", "\\begin"]
    return any(k in s for k in keys)

def generate_slide_image(prompt: str, out_dir: str) -> Optional[str]:
    """Try OpenAI first; retry with simplified prompt; fallback to Gemini."""
    img, err = generate_image_openai(prompt, out_dir)

    if not img:
        print(f"❌ OpenAI failed ({err}); retrying with simplified prompt...")
        simple_prompt = simplify_visual_prompt(prompt)
        img, err = generate_image_openai(simple_prompt, out_dir)

    if not img:
        print(f"🎨 (Fallback) Generating image with Gemini → {prompt[:60]}...")
        img, err = generate_image_gemini(prompt, out_dir)
        if not img:
            print("⚠️ Gemini returned no valid image data.")

    return img


# -------------------------------
# Robust theme background (replace previous set_theme_background)
# -------------------------------
def set_theme_background(slide, prs: Presentation):
    """
    Use slide.background.fill which is more reliable than inserting a rectangle
    shape (avoids z-order/placeholder issues).
    """
    try:
        slide.background.fill.solid()
        slide.background.fill.fore_color.rgb = THEME_BG
    except Exception as e:
        # Fallback to rectangle if background API unavailable
        rect = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, 0, 0, prs.slide_width, prs.slide_height)
        rect.fill.solid()
        rect.fill.fore_color.rgb = THEME_BG
        rect.line.fill.background()
        try:
            rect.element.getparent().insert(0, rect.element)
        except Exception:
            pass

# -------------------------------
# Matplotlib renderer tweak (avoid clipping of math images)
# -------------------------------
def render_text_to_png(text: str, out_dir: str, prefix: str = "txt") -> str:
    """
    Render the given text into a PNG using matplotlib.
    Improved padding and DPI to reduce clipping and improve text quality.
    """
    out_dir_path = Path(out_dir)
    out_dir_path.mkdir(parents=True, exist_ok=True)
    fname = f"{prefix}_{uuid.uuid4().hex[:8]}.png"
    path = out_dir_path / fname

    # Wrap and count lines for figure sizing
    wrapped = "\n".join(textwrap.wrap(text, width=70))
    lines = wrapped.count("\n") + 1

    # Heuristic figure size
    width_in = 10
    height_in = max(1.0, min(8.0, 0.6 * lines))

    # Create figure and place text with padding
    fig = plt.figure(figsize=(width_in, height_in), dpi=200)
    plt.axis("off")
    plt.text(0, 1, wrapped, fontsize=20, va="top", ha="left", wrap=True, family="serif")

    # Tight bbox with extra padding to avoid cropping ascenders/descenders
    fig.tight_layout(pad=0.8)
    plt.savefig(path, bbox_inches="tight", pad_inches=0.35, transparent=True)
    plt.close(fig)

    # Convert to RGBA to avoid pptx insertion issues
    with Image.open(path) as im:
        im = im.convert("RGBA")
        im.save(path)

    return str(path)

def add_logo(slide, prs: Presentation):
    if not os.path.exists(COMPANY_LOGO_PATH):
        return
    try:
        width = Inches(1.25)
        top = Inches(0.35)
        right_margin = Inches(0.35)
        left = prs.slide_width - right_margin - width
        slide.shapes.add_picture(COMPANY_LOGO_PATH, left, top, width=width)
    except Exception as e:
        print(f"⚠️ Logo placement error: {e}")


# -------------------------------
# Title slide (replace existing)
# -------------------------------
def build_title_slide(prs: Presentation, title: str, out_dir: str):
    slide = prs.slides.add_slide(prs.slide_layouts[6])  # blank
    set_theme_background(slide, prs)
    add_logo(slide, prs)

    # Use vertical center, but ensure top margin so text doesn't clip
    left = Inches(1)
    top = Inches(1.2)
    width = prs.slide_width - Inches(2)
    height = Inches(3.0)

    if detect_latex(title):
        img = render_text_to_png(title, out_dir, prefix="title")
        # Fit width but keep some margin so title doesn't touch edges
        img_w = prs.slide_width - Inches(2.0)
        img_h = Inches(2.4)
        img_left = Inches(1)
        img_top = (prs.slide_height - img_h) / 2
        slide.shapes.add_picture(img, img_left, img_top, width=img_w)
    else:
        box = slide.shapes.add_textbox(left, top, width, height)
        tf = box.text_frame
        tf.clear()
        tf.vertical_anchor = MSO_ANCHOR.TOP
        p = tf.paragraphs[0]
        p.text = title
        p.font.name = "Calibri"
        p.font.size = Pt(44)
        p.font.bold = True
        p.font.color.rgb = THEME_TEXT
        p.alignment = PP_ALIGN.CENTER
        # small padding so top lines don't touch edge
        tf.margin_top = Pt(6)
        tf.margin_bottom = Pt(6)


# -------------------------------
# Question slide (replace existing)
# -------------------------------
def build_question_slide(prs: Presentation, q: dict, out_dir: str):
    slide = prs.slides.add_slide(prs.slide_layouts[6])
    set_theme_background(slide, prs)
    add_logo(slide, prs)

    L_MARGIN, R_MARGIN, TOP, BOTTOM = Inches(0.9), Inches(0.9), Inches(1.0), Inches(0.7)
    question_text = q.get("content") or q.get("question") or ""

    # Top area: question (render math to image if needed)
    q_top = TOP
    q_width = prs.slide_width - L_MARGIN - R_MARGIN - Inches(4.8)  # leave space for image at right
    q_height = Inches(2.0)

    if detect_latex(question_text):
        img = render_text_to_png(question_text, out_dir, prefix=f"q{q.get('slide_no','')}")
        # Place image slightly lower to avoid overlap with logo
        slide.shapes.add_picture(img, L_MARGIN, q_top, width=q_width)
        options_top = q_top + q_height + Inches(0.2)
    else:
        q_box = slide.shapes.add_textbox(L_MARGIN, q_top, q_width, q_height)
        q_tf = q_box.text_frame
        q_tf.clear()
        q_tf.vertical_anchor = MSO_ANCHOR.TOP
        q_tf.word_wrap = True
        q_tf.margin_top = Pt(6)
        p = q_tf.paragraphs[0]
        p.text = question_text
        p.font.name = "Calibri"
        p.font.size = Pt(26)
        p.font.bold = True
        p.font.color.rgb = THEME_TEXT
        options_top = q_top + q_height + Inches(0.2)

    # Options box
    opt_box = slide.shapes.add_textbox(L_MARGIN, options_top, q_width, prs.slide_height - options_top - BOTTOM)
    tf_b = opt_box.text_frame
    tf_b.clear()
    tf_b.word_wrap = True
    tf_b.margin_top = Pt(4)

    opts = q.get("options") or []
    # Allow options if they are string separated by newlines
    if isinstance(opts, str):
        opts = [o.strip() for o in opts.splitlines() if o.strip()]

    for opt in opts:
        op = tf_b.add_paragraph()
        op.text = f"• {opt}"
        op.font.name = "Calibri"
        op.font.size = Pt(20)
        op.font.color.rgb = THEME_TEXT_SOFT
        op.space_after = Pt(4)

    # Correct answer (optional) — hide on the main slide if you prefer, comment out to remove
    correct = q.get("correct_answer", "")
    if correct:
        c = tf_b.add_paragraph()
        c.text = f"✅ Correct: {correct}"
        c.font.name = "Calibri"
        c.font.size = Pt(20)
        c.font.bold = True
        c.font.color.rgb = RGBColor(0, 128, 0)

    # Add extracted image to right side (if present)
    image_ref = q.get("image_reference")
    if q.get("has_image") and image_ref:
        if os.path.exists(image_ref):
            try:
                img_w = Inches(4.6)
                img_left = prs.slide_width - img_w - R_MARGIN
                img_top = q_top
                slide.shapes.add_picture(image_ref, img_left, img_top, width=img_w)
            except Exception as e:
                print(f"⚠️ Could not add question image: {e}")

    # Speaker notes
    notes_text = q.get("speaker_notes")
    if notes_text:
        slide.notes_slide.notes_text_frame.text = notes_text


# -------------------------------
# Explanation slide (replace existing)
# -------------------------------
def build_explanation_slide(prs: Presentation, q: dict, out_dir: str):
    slide = prs.slides.add_slide(prs.slide_layouts[6])
    set_theme_background(slide, prs)
    add_logo(slide, prs)

    L_MARGIN = Inches(0.9)
    R_MARGIN = Inches(0.9)
    TOP = Inches(1.0)

    # Title
    title_box = slide.shapes.add_textbox(L_MARGIN, TOP, prs.slide_width - L_MARGIN - R_MARGIN, Inches(0.6))
    tf = title_box.text_frame
    tf.clear()
    tf.vertical_anchor = MSO_ANCHOR.TOP
    p = tf.paragraphs[0]
    p.text = "Explanation"
    p.font.name = "Calibri"
    p.font.size = Pt(28)
    p.font.bold = True
    p.font.color.rgb = THEME_TEXT

    content_top = TOP + Inches(0.9)
    text_width = prs.slide_width * 0.58 - Inches(0.3)
    text_height = prs.slide_height - Inches(2.0)

    exp_text = q.get("refined_explanation") or q.get("explanation") or "Explanation not available."

    if detect_latex(exp_text):
        img = render_text_to_png(exp_text, out_dir, prefix=f"exp{q.get('slide_no','')}")
        # Make sure image gets margin so it doesn't touch edges
        slide.shapes.add_picture(img, L_MARGIN, content_top, width=prs.slide_width - L_MARGIN - R_MARGIN)
    else:
        exp_box = slide.shapes.add_textbox(L_MARGIN, content_top, text_width, text_height)
        tf2 = exp_box.text_frame
        tf2.clear()
        tf2.vertical_anchor = MSO_ANCHOR.TOP
        tf2.word_wrap = True
        tf2.margin_top = Pt(4)
        for line in [s.strip() for s in re.split(r'\.\s+', exp_text) if s.strip()]:
            para = tf2.add_paragraph()
            para.text = f"• {line.rstrip('.')}"
            para.font.name = "Calibri"
            para.font.size = Pt(20)
            para.font.color.rgb = THEME_TEXT_SOFT
            para.space_after = Pt(6)

    # Optional image on right side (if exists)
    if q.get("has_image") and q.get("image_reference") and os.path.exists(q.get("image_reference")):
        try:
            img_width = Inches(4.6)
            img_left = prs.slide_width - img_width - R_MARGIN
            img_top = content_top
            slide.shapes.add_picture(q.get("image_reference"), img_left, img_top, width=img_width)
        except Exception as e:
            print(f"⚠️ Could not add extracted image to explanation: {e}")

    # Speaker notes
    notes_text = q.get("speaker_notes")
    if notes_text:
        slide.notes_slide.notes_text_frame.text = notes_text


# -------------------------------
# MAIN LOGIC
# -------------------------------
def add_title_slide(prs, title_text):
    slide_layout = prs.slide_layouts[0]
    slide = prs.slides.add_slide(slide_layout)
    slide.shapes.title.text = title_text
    slide.placeholders[1].text = "Generated by MCQ Generator"
    return slide

def add_question_slide(prs, question_data):
    slide_layout = prs.slide_layouts[1]
    slide = prs.slides.add_slide(slide_layout)
    slide.shapes.title.text = question_data.get("question", "")
    tf = slide.placeholders[1].text_frame
    tf.clear()
    for opt in question_data.get("options", []):
        tf.add_paragraph().text = opt
    return slide

def add_explanation_slide(prs, question_data, image_path=None):
    slide_layout = prs.slide_layouts[6]  # blank layout
    slide = prs.slides.add_slide(slide_layout)
    left, top, width, height = Inches(1), Inches(1), Inches(11), Inches(5)
    textbox = slide.shapes.add_textbox(left, top, width, height)
    p = textbox.text_frame.add_paragraph()
    p.text = question_data.get("refined_explanation", "")
    if image_path and os.path.exists(image_path):
        slide.shapes.add_picture(image_path, Inches(8), Inches(3), Inches(3), Inches(2))
    return slide


# -------------------------------
# Main generation function
# -------------------------------
@celery.task(name="video_main.create_mcq_ppt")
def create_mcq_ppt(data: dict, out_path: Optional[str] = None, pdf_out: Optional[str] = None):
    """Generate PPTX from MCQ JSON structure.

    `data` expected to be the JSON dict (NOT the outer celery envelope). Example keys: id, json (the actual payload).
    """
    if not data:
        raise ValueError("No data provided")

    session_id = data.get("id") or uuid.uuid4().hex[:8]
    payload = data.get("json") if data.get("json") else data

    out_dir = Path(f"./public/tmp/{session_id}/PPT")
    out_dir.mkdir(parents=True, exist_ok=True)
    images_dir = out_dir / "ppt_images"
    images_dir.mkdir(parents=True, exist_ok=True)

    if out_path is None:
        out_path = out_dir / f"{session_id}.pptx"
    else:
        out_path = Path(out_path)

    if pdf_out is None:
        pdf_out = out_dir / f"{session_id}.pdf"
    else:
        pdf_out = Path(pdf_out)

    title = payload.get("title", "Presentation")
    slides = payload.get("slides", [])

    prs = Presentation()
    prs.slide_width = Inches(13.333)
    prs.slide_height = Inches(7.5)

    # Build title slide
    build_title_slide(prs, title, str(images_dir))

    for q in slides:
        build_question_slide(prs, q, str(images_dir))
        build_explanation_slide(prs, q, str(images_dir))

    # Save
    prs.save(str(out_path))
    print(f"✅ Saved presentation → {out_path}")

    # Convert PPTX to PDF (LibreOffice) if available
    candidates = [
        os.getenv("LIBREOFFICE_BIN"),
        os.getenv("LIBREOFFICE"),
        shutil.which("soffice"),
        shutil.which("libreoffice"),
    ]
    binpath = next((p for p in candidates if p), None)
    if binpath and os.path.exists(binpath):
        try:
            subprocess.run([binpath, "--headless", "--convert-to", "pdf", str(out_path), "--outdir", str(out_dir)], check=True)
            print(f"✅ Saved PDF → {pdf_out}")
        except Exception as e:
            print(f"⚠️ Error converting to PDF: {e}")
    else:
        print("⚠️ LibreOffice not found. Skipping PDF conversion.")

    return {"pdf": str(pdf_out), "id": session_id}



# if __name__ == "__main__":
#     create_mcq_ppt(str(JSON_PATH), str(PPT_PATH), str(PDF_PATH))
