"""ARQ worker task: generate a presentation end-to-end."""
import asyncio
import math
import random
import traceback
import uuid
from datetime import datetime
from typing import List

import dirtyjson
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession

from models.sql.presentation import PresentationModel
from models.presentation_outline_model import PresentationOutlineModel, SlideOutlineModel
from models.presentation_structure_model import PresentationStructureModel
from models.sql.slide import SlideModel
from models.sql.job import JobModel
from services.brand_enforcement_service import BrandEnforcementService
from services.content_intelligence_service import ContentIntelligenceService
from services.database import async_session_maker
from services.image_generation_service import ImageGenerationService
from services.redis_service import publish_job_progress
from services.slide_mapping_engine import SlideMappingEngine
from utils.asset_directory_utils import get_images_directory
from utils.export_utils import export_presentation
from utils.get_layout_by_name import get_layout_by_name
from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline
from utils.llm_calls.generate_presentation_structure import generate_presentation_structure
from utils.llm_calls.generate_slide_content import get_slide_content_from_type_and_outline
from utils.ppt_utils import get_presentation_title_from_outlines
from utils.process_slides import process_slide_and_fetch_assets


async def generate_presentation_task(ctx: dict, job_id: str) -> None:
    """ARQ task: full presentation generation pipeline."""
    job_uuid = uuid.UUID(job_id)

    async with async_session_maker() as session:
        job = await session.get(JobModel, job_uuid)
        if not job:
            return

        try:
            job.status = "processing"
            job.started_at = datetime.utcnow()
            job.progress = 0
            job.progress_message = "Starting generation"
            await session.commit()
            await _publish(job_uuid, 0, "Starting generation")

            # Load the stored request data from the presentation record
            presentation = await session.get(PresentationModel, job.presentation_id)
            if not presentation:
                raise ValueError("Presentation record not found")

            # Extract request parameters from the stored presentation
            content = presentation.content or ""
            n_slides = presentation.n_slides or 10
            language = presentation.language or "en"
            tone = presentation.tone or "professional"
            verbosity = presentation.verbosity or "standard"
            instructions = presentation.instructions
            template = getattr(presentation, "template_name", None) or "general"
            include_title_slide = getattr(presentation, "include_title_slide", True)

            # --- Step 1: Brand context ---
            brand_context = ""
            if job.client_id:
                brand_svc = BrandEnforcementService()
                brand_context = await brand_svc.get_brand_context_for_llm(
                    job.client_id, session
                )

            await _update_job(session, job, 5, "Analyzing content")

            # --- Step 2: Content intelligence (if raw content provided) ---
            content_summary = None
            if content and len(content) > 100:
                ci_service = ContentIntelligenceService()
                classified = await ci_service.classify(content)
                content_summary = classified.summary

            await _update_job(session, job, 10, "Generating outlines")

            # --- Step 3: Generate outlines ---
            presentation_outlines_text = ""
            async for chunk in generate_ppt_outline(
                content,
                n_slides,
                language,
                None,  # additional_context
                tone,
                verbosity,
                instructions,
                include_title_slide,
                False,  # web_search
                brand_context=brand_context,
                content_summary=content_summary,
            ):
                if isinstance(chunk, HTTPException):
                    raise chunk
                presentation_outlines_text += chunk

            try:
                outlines_json = dict(dirtyjson.loads(presentation_outlines_text))
            except Exception:
                raise ValueError("Failed to parse generated outlines")

            presentation_outlines = PresentationOutlineModel(**outlines_json)
            total_outlines = n_slides

            await _update_job(session, job, 25, "Selecting layouts")

            # --- Step 4: Layout selection ---
            layout_model = await get_layout_by_name(template)
            total_slide_layouts = len(layout_model.slides)

            if layout_model.ordered:
                presentation_structure = layout_model.to_presentation_structure()
            else:
                presentation_structure = await generate_presentation_structure(
                    presentation_outlines, layout_model, instructions
                )

            presentation_structure.slides = presentation_structure.slides[:total_outlines]
            for index in range(total_outlines):
                random_slide_index = random.randint(0, total_slide_layouts - 1)
                if index >= len(presentation_structure.slides):
                    presentation_structure.slides.append(random_slide_index)
                elif presentation_structure.slides[index] >= total_slide_layouts:
                    presentation_structure.slides[index] = random_slide_index

            # Update presentation model with outlines & structure
            presentation.title = get_presentation_title_from_outlines(presentation_outlines)
            presentation.outlines = presentation_outlines.model_dump()
            presentation.layout = layout_model.model_dump()
            presentation.structure = presentation_structure.model_dump()
            await session.commit()

            await _update_job(session, job, 35, "Generating slides")

            # --- Step 5: Generate slide content ---
            image_generation_service = ImageGenerationService(get_images_directory())
            async_assets_generation_tasks = []
            slides: List[SlideModel] = []

            slide_layout_indices = presentation_structure.slides
            slide_layouts = [layout_model.slides[idx] for idx in slide_layout_indices]

            batch_size = 10
            for start in range(0, len(slide_layouts), batch_size):
                end = min(start + batch_size, len(slide_layouts))

                content_tasks = [
                    get_slide_content_from_type_and_outline(
                        slide_layouts[i],
                        presentation_outlines.slides[i],
                        language,
                        tone,
                        verbosity,
                        instructions,
                        brand_context=brand_context,
                    )
                    for i in range(start, end)
                ]
                batch_contents = await asyncio.gather(*content_tasks)

                batch_slides = []
                for offset, slide_content in enumerate(batch_contents):
                    i = start + offset
                    slide = SlideModel(
                        presentation=job.presentation_id,
                        layout_group=layout_model.name,
                        layout=slide_layouts[i].id,
                        index=i,
                        speaker_note=slide_content.get("__speaker_note__"),
                        content=slide_content,
                    )
                    slides.append(slide)
                    batch_slides.append(slide)

                asset_tasks = [
                    process_slide_and_fetch_assets(image_generation_service, slide)
                    for slide in batch_slides
                ]
                async_assets_generation_tasks.extend(asset_tasks)

                pct = 35 + int((end / len(slide_layouts)) * 40)
                await _update_job(session, job, pct, f"Generating slide {end}/{len(slide_layouts)}")

            await _update_job(session, job, 80, "Fetching assets")

            # --- Step 6: Fetch assets ---
            generated_assets_list = await asyncio.gather(*async_assets_generation_tasks)
            generated_assets = []
            for assets_list in generated_assets_list:
                generated_assets.extend(assets_list)

            await _update_job(session, job, 90, "Saving presentation")

            # --- Step 7: Save ---
            session.add(presentation)
            session.add_all(slides)
            session.add_all(generated_assets)
            await session.commit()

            await _update_job(session, job, 95, "Exporting PPTX")

            # --- Step 8: Export ---
            await export_presentation(
                job.presentation_id,
                presentation.title or str(uuid.uuid4()),
                "pptx",
            )

            # --- Done ---
            job.status = "completed"
            job.progress = 100
            job.progress_message = "Generation complete"
            job.completed_at = datetime.utcnow()
            await session.commit()
            await _publish(job_uuid, 100, "Generation complete", "completed")

        except Exception as e:
            traceback.print_exc()
            job.status = "failed"
            job.error_message = str(e)[:500]
            job.progress_message = "Generation failed"
            job.completed_at = datetime.utcnow()
            await session.commit()
            await _publish(job_uuid, job.progress, "Generation failed", "failed")


async def _update_job(
    session: AsyncSession, job: JobModel, progress: int, message: str
) -> None:
    job.progress = progress
    job.progress_message = message
    await session.commit()
    await publish_job_progress(job.id, progress, message)


async def _publish(
    job_id: uuid.UUID, progress: int, message: str, status: str = "processing"
) -> None:
    try:
        await publish_job_progress(job_id, progress, message, status)
    except Exception:
        pass  # Redis unavailable is not fatal
