"""Recipe service - business logic for copy-on-write versioning."""
import math
from typing import Optional

from fastapi import HTTPException, status
from sqlalchemy import func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from src.backend.models.orm.measurement import Measurement
from src.backend.models.orm.recipe import Recipe, RecipeVersion
from src.backend.models.orm.setting import RecipeVersionAudit
from src.backend.models.orm.task import RecipeSubtask, RecipeTask
from src.backend.models.orm.user import User
from src.backend.models.api.recipe import RecipeCreate, RecipeUpdate


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

async def _get_recipe_or_404(db: AsyncSession, recipe_id: int) -> Recipe:
    """Return a recipe or raise 404."""
    result = await db.execute(select(Recipe).where(Recipe.id == recipe_id))
    recipe = result.scalar_one_or_none()
    if recipe is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Recipe not found")
    return recipe


async def _get_current_version(
    db: AsyncSession, recipe_id: int
) -> Optional[RecipeVersion]:
    """Return the current version for a recipe (with tasks/subtasks loaded)."""
    result = await db.execute(
        select(RecipeVersion)
        .where(
            RecipeVersion.recipe_id == recipe_id,
            RecipeVersion.is_current == True,  # noqa: E712
        )
        .options(
            selectinload(RecipeVersion.tasks).selectinload(RecipeTask.subtasks)
        )
    )
    return result.scalar_one_or_none()


async def _copy_tasks_to_version(
    db: AsyncSession,
    source_version: RecipeVersion,
    target_version: RecipeVersion,
) -> None:
    """Deep-copy all tasks and subtasks from *source* to *target* version."""
    for task in source_version.tasks:
        new_task = RecipeTask(
            version_id=target_version.id,
            order_index=task.order_index,
            title=task.title,
            directive=task.directive,
            description=task.description,
            file_path=task.file_path,
            file_type=task.file_type,
            annotations_json=task.annotations_json,
        )
        db.add(new_task)
        await db.flush()  # get new_task.id

        for sub in task.subtasks:
            new_sub = RecipeSubtask(
                task_id=new_task.id,
                marker_number=sub.marker_number,
                description=sub.description,
                measurement_type=sub.measurement_type,
                nominal=sub.nominal,
                utl=sub.utl,
                uwl=sub.uwl,
                lwl=sub.lwl,
                ltl=sub.ltl,
                unit=sub.unit,
                image_path=sub.image_path,
            )
            db.add(new_sub)

    await db.flush()


async def _write_audit(
    db: AsyncSession,
    recipe_id: int,
    old_version_id: Optional[int],
    new_version_id: int,
    changed_by: int,
    change_type: str,
    change_reason: Optional[str] = None,
) -> None:
    """Persist an audit record for a version change."""
    audit = RecipeVersionAudit(
        recipe_id=recipe_id,
        old_version_id=old_version_id,
        new_version_id=new_version_id,
        changed_by=changed_by,
        change_type=change_type,
        change_reason=change_reason,
    )
    db.add(audit)
    await db.flush()


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

async def create_recipe(
    db: AsyncSession,
    data: RecipeCreate,
    user: User,
) -> Recipe:
    """Create a recipe together with its first version (v1).

    Returns the newly created Recipe with the current version loaded.
    """
    # Check code uniqueness
    existing = await db.execute(
        select(Recipe).where(Recipe.code == data.code)
    )
    if existing.scalar_one_or_none() is not None:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Recipe code '{data.code}' already exists",
        )

    recipe = Recipe(
        code=data.code,
        name=data.name,
        description=data.description,
        image_path=data.image_path,
        measurement_interval_minutes=data.measurement_interval_minutes,
        created_by=user.id,
    )
    db.add(recipe)
    await db.flush()

    version = RecipeVersion(
        recipe_id=recipe.id,
        version_number=1,
        is_current=True,
        created_by=user.id,
        change_notes="Initial version",
    )
    db.add(version)
    await db.flush()

    await _write_audit(
        db,
        recipe_id=recipe.id,
        old_version_id=None,
        new_version_id=version.id,
        changed_by=user.id,
        change_type="CREATE",
        change_reason="Recipe created",
    )

    # Reload recipe with versions + tasks eagerly loaded
    result = await db.execute(
        select(Recipe)
        .where(Recipe.id == recipe.id)
        .options(
            selectinload(Recipe.versions)
            .selectinload(RecipeVersion.tasks)
            .selectinload(RecipeTask.subtasks)
        )
    )
    return result.scalar_one()


async def create_new_version(
    db: AsyncSession,
    recipe_id: int,
    data: RecipeUpdate,
    user: User,
) -> RecipeVersion:
    """Copy-on-write: create a new version by cloning current tasks/subtasks.

    1. Fetch current version (with tasks + subtasks)
    2. Create a new RecipeVersion with incremented version_number
    3. Deep-copy all tasks + subtasks into the new version
    4. Apply updates from *data* to the recipe header
    5. Flip is_current: old -> False, new -> True
    6. Write audit trail
    """
    recipe = await _get_recipe_or_404(db, recipe_id)
    if not recipe.active:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Cannot update an inactive recipe",
        )

    current = await _get_current_version(db, recipe_id)
    if current is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="No current version found for this recipe",
        )

    old_version_id = current.id
    new_version_number = current.version_number + 1

    # Mark old version as non-current
    await db.execute(
        update(RecipeVersion)
        .where(RecipeVersion.id == current.id)
        .values(is_current=False)
    )

    # Create new version
    new_version = RecipeVersion(
        recipe_id=recipe_id,
        version_number=new_version_number,
        is_current=True,
        created_by=user.id,
        change_notes=data.change_notes,
    )
    db.add(new_version)
    await db.flush()

    # Deep-copy tasks + subtasks
    await _copy_tasks_to_version(db, source_version=current, target_version=new_version)

    # Apply task-level updates (file_path, annotations_json) to the first task
    if data.file_path is not None or data.annotations_json is not None:
        # Reload new version's tasks
        result_tasks = await db.execute(
            select(RecipeTask)
            .where(RecipeTask.version_id == new_version.id)
            .order_by(RecipeTask.order_index)
        )
        tasks = list(result_tasks.scalars().all())

        if tasks:
            first_task = tasks[0]
            if data.file_path is not None:
                first_task.file_path = data.file_path
                # Auto-detect file_type from extension
                if data.file_type:
                    first_task.file_type = data.file_type
                elif data.file_path.lower().endswith(".pdf"):
                    first_task.file_type = "pdf"
                else:
                    first_task.file_type = "image"
            if data.annotations_json is not None:
                first_task.annotations_json = data.annotations_json
        else:
            # No tasks yet — create a default task to hold the drawing
            default_task = RecipeTask(
                version_id=new_version.id,
                order_index=0,
                title="Technical Drawing",
                file_path=data.file_path,
                file_type=data.file_type or (
                    "pdf" if data.file_path and data.file_path.lower().endswith(".pdf")
                    else "image"
                ),
                annotations_json=data.annotations_json,
            )
            db.add(default_task)

        await db.flush()

    # Apply header updates
    update_fields: dict = {}
    if data.name is not None:
        update_fields["name"] = data.name
    if data.description is not None:
        update_fields["description"] = data.description
    if data.image_path is not None:
        update_fields["image_path"] = data.image_path
    if data.measurement_interval_minutes is not None:
        update_fields["measurement_interval_minutes"] = data.measurement_interval_minutes
    if update_fields:
        await db.execute(
            update(Recipe).where(Recipe.id == recipe_id).values(**update_fields)
        )

    # Audit
    await _write_audit(
        db,
        recipe_id=recipe_id,
        old_version_id=old_version_id,
        new_version_id=new_version.id,
        changed_by=user.id,
        change_type="UPDATE",
        change_reason=data.change_notes,
    )

    await db.flush()

    # Reload the version with tasks for the response
    result = await db.execute(
        select(RecipeVersion)
        .where(RecipeVersion.id == new_version.id)
        .options(
            selectinload(RecipeVersion.tasks).selectinload(RecipeTask.subtasks)
        )
    )
    return result.scalar_one()


async def get_current_version(
    db: AsyncSession, recipe_id: int
) -> RecipeVersion:
    """Return the current version with tasks and subtasks.

    Raises 404 if the recipe or its current version cannot be found.
    """
    await _get_recipe_or_404(db, recipe_id)
    version = await _get_current_version(db, recipe_id)
    if version is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="No current version found for this recipe",
        )
    return version


async def get_measurement_count(
    db: AsyncSession, recipe_id: int, version_number: int
) -> int:
    """Return the number of measurements recorded against a specific version."""
    # Resolve version_id from recipe_id + version_number
    result = await db.execute(
        select(RecipeVersion.id).where(
            RecipeVersion.recipe_id == recipe_id,
            RecipeVersion.version_number == version_number,
        )
    )
    version_id = result.scalar_one_or_none()
    if version_id is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Version {version_number} not found for recipe {recipe_id}",
        )

    count_result = await db.execute(
        select(func.count()).select_from(Measurement).where(
            Measurement.version_id == version_id
        )
    )
    return count_result.scalar_one()


async def version_has_measurements(db: AsyncSession, version_id: int) -> bool:
    """Check if any measurements exist for the given version."""
    result = await db.execute(
        select(func.count()).select_from(Measurement).where(
            Measurement.version_id == version_id
        )
    )
    return result.scalar_one() > 0


async def update_current_version(
    db: AsyncSession,
    recipe_id: int,
    data: RecipeUpdate,
) -> RecipeVersion:
    """Update recipe header in-place on the current version (no copy-on-write)."""
    # Apply header updates (name, description, image_path)
    update_fields: dict = {}
    if data.name is not None:
        update_fields["name"] = data.name
    if data.description is not None:
        update_fields["description"] = data.description
    if data.image_path is not None:
        update_fields["image_path"] = data.image_path
    if data.measurement_interval_minutes is not None:
        update_fields["measurement_interval_minutes"] = data.measurement_interval_minutes
    if update_fields:
        await db.execute(
            update(Recipe).where(Recipe.id == recipe_id).values(**update_fields)
        )

    # Apply task-level file updates to first task (same logic as create_new_version)
    current = await _get_current_version(db, recipe_id)
    if data.file_path is not None or data.annotations_json is not None:
        result_tasks = await db.execute(
            select(RecipeTask)
            .where(RecipeTask.version_id == current.id)
            .order_by(RecipeTask.order_index)
        )
        tasks = list(result_tasks.scalars().all())
        if tasks:
            first_task = tasks[0]
            if data.file_path is not None:
                first_task.file_path = data.file_path
                if data.file_type:
                    first_task.file_type = data.file_type
                elif data.file_path.lower().endswith(".pdf"):
                    first_task.file_type = "pdf"
                else:
                    first_task.file_type = "image"
            if data.annotations_json is not None:
                first_task.annotations_json = data.annotations_json
        else:
            # No tasks yet — create a default task to hold the drawing
            default_task = RecipeTask(
                version_id=current.id,
                order_index=0,
                title="Technical Drawing",
                file_path=data.file_path,
                file_type=data.file_type or (
                    "pdf" if data.file_path and data.file_path.lower().endswith(".pdf")
                    else "image"
                ),
                annotations_json=data.annotations_json,
            )
            db.add(default_task)

    await db.flush()

    # Reload version with tasks for response
    result = await db.execute(
        select(RecipeVersion)
        .where(RecipeVersion.id == current.id)
        .options(
            selectinload(RecipeVersion.tasks).selectinload(RecipeTask.subtasks)
        )
    )
    return result.scalar_one()


async def list_recipes(
    db: AsyncSession,
    page: int = 1,
    per_page: int = 20,
    search: Optional[str] = None,
) -> dict:
    """Return a paginated list of active recipes.

    Each recipe includes its current version (with tasks).
    """
    base_filter = [Recipe.active == True]  # noqa: E712
    if search:
        like_pattern = f"%{search}%"
        base_filter.append(
            (Recipe.name.ilike(like_pattern)) | (Recipe.code.ilike(like_pattern))
        )

    # Total count
    count_stmt = select(func.count()).select_from(Recipe).where(*base_filter)
    total = (await db.execute(count_stmt)).scalar_one()

    pages = max(1, math.ceil(total / per_page))
    offset = (page - 1) * per_page

    # Fetch recipes with versions and tasks for thumbnail display
    stmt = (
        select(Recipe)
        .where(*base_filter)
        .options(
            selectinload(Recipe.versions)
            .selectinload(RecipeVersion.tasks)
        )
        .order_by(Recipe.name.asc())
        .offset(offset)
        .limit(per_page)
    )
    result = await db.execute(stmt)
    recipes = result.scalars().all()

    return {
        "items": recipes,
        "total": total,
        "page": page,
        "per_page": per_page,
        "pages": pages,
    }


async def get_recipe_detail(db: AsyncSession, recipe_id: int) -> Recipe:
    """Return a single recipe with its current version + tasks loaded."""
    result = await db.execute(
        select(Recipe)
        .where(Recipe.id == recipe_id)
        .options(
            selectinload(Recipe.versions)
            .selectinload(RecipeVersion.tasks)
            .selectinload(RecipeTask.subtasks)
        )
    )
    recipe = result.scalar_one_or_none()
    if recipe is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Recipe not found"
        )
    return recipe


async def get_recipe_by_code(db: AsyncSession, code: str) -> Recipe:
    """Return a recipe by its barcode/code, with current version loaded."""
    result = await db.execute(
        select(Recipe)
        .where(Recipe.code == code, Recipe.active == True)  # noqa: E712
        .options(
            selectinload(Recipe.versions)
            .selectinload(RecipeVersion.tasks)
            .selectinload(RecipeTask.subtasks)
        )
    )
    recipe = result.scalar_one_or_none()
    if recipe is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No active recipe found with code '{code}'",
        )
    return recipe


async def deactivate_recipe(
    db: AsyncSession, recipe_id: int, user: User
) -> Recipe:
    """Soft-delete a recipe by setting active=False."""
    recipe = await _get_recipe_or_404(db, recipe_id)
    if not recipe.active:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Recipe is already inactive",
        )

    await db.execute(
        update(Recipe).where(Recipe.id == recipe_id).values(active=False)
    )

    # Audit
    current = await _get_current_version(db, recipe_id)
    if current is not None:
        await _write_audit(
            db,
            recipe_id=recipe_id,
            old_version_id=current.id,
            new_version_id=current.id,
            changed_by=user.id,
            change_type="RETIRE",
            change_reason="Recipe deactivated",
        )

    await db.flush()
    await db.refresh(recipe)
    return recipe


async def list_versions(
    db: AsyncSession, recipe_id: int
) -> list[RecipeVersion]:
    """Return all versions of a recipe, ordered by version_number desc."""
    await _get_recipe_or_404(db, recipe_id)

    result = await db.execute(
        select(RecipeVersion)
        .where(RecipeVersion.recipe_id == recipe_id)
        .order_by(RecipeVersion.version_number.desc())
        .options(
            selectinload(RecipeVersion.tasks).selectinload(RecipeTask.subtasks)
        )
    )
    return list(result.scalars().all())


async def get_version_detail(
    db: AsyncSession, recipe_id: int, version_number: int
) -> RecipeVersion:
    """Return a specific version of a recipe with tasks loaded."""
    await _get_recipe_or_404(db, recipe_id)

    result = await db.execute(
        select(RecipeVersion)
        .where(
            RecipeVersion.recipe_id == recipe_id,
            RecipeVersion.version_number == version_number,
        )
        .options(
            selectinload(RecipeVersion.tasks).selectinload(RecipeTask.subtasks)
        )
    )
    version = result.scalar_one_or_none()
    if version is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Version {version_number} not found for recipe {recipe_id}",
        )
    return version
