from pathlib import Path

from fastapi import HTTPException, status
from sqlmodel import Session

from app.core.logging import setup_logger
from app.core.constants import ERR_NOT_FOUND, ERR_FIELD_UPDATE, ERR_UNEXPECTED
from app.models.project_master import ProjectMaster
from app.repositories.project_repo import ProjectRepository
from app.repositories.product_image_repo import ProductImageRepository
from app.utils.image import (
    validate_overlay_file,
    validate_logo_file,
    save_upload_file_streamed,
    create_thumbnail_from_path,
    safe_delete,
)
from app.utils.paths import (
    ASSETS_ROOT,
    project_dir,
    overlay_dir,
    overlay_thumb_dir,
    logo_dir,
    logo_thumb_dir,
    final_dir,
    final_thumb_dir,
    rel_path,
    abs_from_db,
    cleanup_relative,
)
from app.utils.image_crop import crop_transparent_canvas
from app.utils.image_background import normalize_white_background

logger = setup_logger(__name__)

class ProjectService:
    """High-level project orchestration."""

    @staticmethod
    async def create_project(
        project_name: str,
        client_name: str,
        overlay_file,
        logo_file,
        current_user,
        session: Session,
    ) -> ProjectMaster:
        """
        Create a project with:
        - Overlay image + overlay thumbnail
        - Optional logo image + logo thumbnail
        - All stored paths are RELATIVE to assets/
        - Guaranteed cleanup on ANY failure
        """

        # Files created so far (for cleanup)
        created_files: list[Path] = []

        # 1. Validate overlay
        try:
            validate_overlay_file(overlay_file)
        except Exception:
            raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERR_UNEXPECTED)

        # Validate logo (optional)
        if logo_file:
            try:
                validate_logo_file(logo_file)
            except Exception:
                raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERR_UNEXPECTED)

        # 1. Create minimal DB row to obtain id
        project = ProjectMaster(
            project_name=project_name,
            client_name=client_name,
            overlay_image_path=None,
            overlay_thumbnail_path=None,
            logo_image_path=None,
            logo_thumbnail_path=None,
        )
        try:
            session.add(project)
            session.commit()
            session.refresh(project)
        except Exception:
            session.rollback()
            logger.exception("Failed to create initial project row")
            raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create project")

        created_files: list[Path] = []
        project_id = project.id

        # Ensure directory tree exists
        try:
            project_dir(project_id).mkdir(parents=True, exist_ok=True)
            overlay_dir(project_id).mkdir(parents=True, exist_ok=True)
            overlay_thumb_dir(project_id).mkdir(parents=True, exist_ok=True)
            logo_dir(project_id).mkdir(parents=True, exist_ok=True)
            logo_thumb_dir(project_id).mkdir(parents=True, exist_ok=True)
            final_dir(project_id).mkdir(parents=True, exist_ok=True)
            final_thumb_dir(project_id).mkdir(parents=True, exist_ok=True)
        except Exception:
            logger.exception("Failed to create project directories for project_id=%s", project_id)
            # Attempt to cleanup DB row
            try:
                session.delete(project)
                session.commit()
            except Exception:
                session.rollback()
            raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create project directories")

        # Save logo (optional)
        logo_abs = None
        logo_thumb_abs = None
        try:
            if logo_file:
                logo_abs = (await save_upload_file_streamed(logo_file, logo_dir(project_id))).resolve()
                created_files.append(logo_abs)
                logo_thumb_abs = create_thumbnail_from_path(logo_abs, logo_thumb_dir(project_id)).resolve()
                created_files.append(logo_thumb_abs)
        except Exception:
            logger.exception("Failed to save or thumbnail logo for project_id=%s", project_id)
            for f in created_files:
                safe_delete(f)
            # Delete DB row
            try:
                session.delete(project)
                session.commit()
            except Exception:
                session.rollback()
            raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to process logo image")

        # Save overlay (required)
        overlay_abs = None
        overlay_thumb_abs = None
        try:
            overlay_abs = (await save_upload_file_streamed(overlay_file, overlay_dir(project_id))).resolve()
            crop_transparent_canvas(overlay_abs)
            normalize_white_background(overlay_abs, target_rgb=(207, 207, 207))
            created_files.append(overlay_abs)
            overlay_thumb_abs = create_thumbnail_from_path(overlay_abs, overlay_thumb_dir(project_id)).resolve()
            created_files.append(overlay_thumb_abs)
        except Exception:
            logger.exception("Failed to save or thumbnail overlay for project_id=%s", project_id)
            for f in created_files:
                safe_delete(f)
            try:
                session.delete(project)
                session.commit()
            except Exception:
                session.rollback()
            raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to save overlay image")

        # Convert saved absolute files -> relative 'assets/...' and update DB
        try:
            project.overlay_image_path = rel_path(overlay_abs)
            project.overlay_thumbnail_path = rel_path(overlay_thumb_abs)
            project.logo_image_path = rel_path(logo_abs) if logo_abs else None
            project.logo_thumbnail_path = rel_path(logo_thumb_abs) if logo_thumb_abs else None

            session.add(project)
            session.commit()
            session.refresh(project)
            return project
        except Exception:
            logger.exception("Failed to update project DB with saved asset paths for project_id=%s", project_id)
            # Cleanup created files and DB row
            for f in created_files:
                safe_delete(f)
            try:
                session.delete(project)
                session.commit()
            except Exception:
                session.rollback()
            raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to save project assets")

    @staticmethod
    def list_projects(session: Session, page: int, limit: int, search: str | None, sort_by: str, order: str):
        return ProjectRepository.list_advanced(
            session=session,
            page=page,
            limit=limit,
            search=search,
            sort_by=sort_by,
            order=order,
        )

    @staticmethod
    def get_project_detail(session: Session, project_id: int):
        project = ProjectRepository.get_project(session, project_id)
        if not project or project.is_deleted:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERR_NOT_FOUND)

        rows = ProductImageRepository.list_with_template(session, project_id)

        images = []
        for img, tpl in rows:
            images.append({
                "id": img.id,
                "final_image_path": img.final_image_path,
                "final_image_thumbnail_path": img.final_image_thumbnail_path,
                "final_image_product_path": img.final_image_product_path,
                "product_template": tpl
            })

        return project, images

    @staticmethod
    async def update_project(
        project_id: int,
        project_name: str | None,
        client_name: str | None,
        overlay_file,
        logo_file,
        current_user,
        db: Session,
    ):
        """
        Update project metadata and optionally regenerate final images when overlay changes.
        Handles template-count changes:
        - If new templates appear -> INSERT new rows
        - Else -> UPDATE existing rows only
        Ensures full rollback and file cleanup on failure.
        """

        project = ProjectRepository.get_project(db, project_id)
        if not project or project.is_deleted:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERR_NOT_FOUND)

        if not any([project_name, client_name, overlay_file, logo_file]):
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERR_FIELD_UPDATE)

        if project_name:
            project.project_name = project_name
        if client_name:
            project.client_name = client_name

        # If only metadata changed
        if not overlay_file and not logo_file:
            db.add(project)
            db.commit()
            db.refresh(project)
            return {
                "success": True,
                "message": "Project updated successfully.",
                "project_id": project.id,
            }

        # Remember old DB paths for cleanup after success
        old_overlay = project.overlay_image_path
        old_overlay_thumb = project.overlay_thumbnail_path
        old_logo = project.logo_image_path
        old_logo_thumb = project.logo_thumbnail_path

        # Save new files into project-specific dirs
        new_overlay_abs = None
        new_overlay_thumb_abs = None
        new_logo_abs = None
        new_logo_thumb_abs = None

        # Ensure dirs exist
        project_dir(project_id).mkdir(parents=True, exist_ok=True)
        overlay_dir(project_id).mkdir(parents=True, exist_ok=True)
        overlay_thumb_dir(project_id).mkdir(parents=True, exist_ok=True)
        logo_dir(project_id).mkdir(parents=True, exist_ok=True)
        logo_thumb_dir(project_id).mkdir(parents=True, exist_ok=True)

        # Process overlay update
        if overlay_file:
            try:
                validate_overlay_file(overlay_file)
                new_overlay_abs = (await save_upload_file_streamed(overlay_file, overlay_dir(project_id))).resolve()
                crop_transparent_canvas(new_overlay_abs)
                normalize_white_background(new_overlay_abs, target_rgb=(207, 207, 207))
                new_overlay_thumb_abs = create_thumbnail_from_path(new_overlay_abs, overlay_thumb_dir(project_id))
            except Exception:
                safe_delete(new_overlay_abs)
                safe_delete(new_overlay_thumb_abs)
                raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update overlay image")

            project.overlay_image_path = rel_path(new_overlay_abs)
            project.overlay_thumbnail_path = rel_path(new_overlay_thumb_abs)

        # Process logo update
        if logo_file:
            try:
                validate_logo_file(logo_file)
                new_logo_abs = (await save_upload_file_streamed(logo_file, logo_dir(project_id))).resolve()
                new_logo_thumb_abs = create_thumbnail_from_path(new_logo_abs, logo_thumb_dir(project_id))
            except Exception:
                safe_delete(new_logo_abs)
                safe_delete(new_logo_thumb_abs)
                raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update logo image")

            project.logo_image_path = rel_path(new_logo_abs)
            project.logo_thumbnail_path = rel_path(new_logo_thumb_abs)

        # Commit DB changes; on failure, remove newly created files
        try:
            db.add(project)
            db.commit()
            db.refresh(project)
        except Exception:
            db.rollback()
            safe_delete(new_overlay_abs)
            safe_delete(new_overlay_thumb_abs)
            safe_delete(new_logo_abs)
            safe_delete(new_logo_thumb_abs)
            logger.exception("Failed to update project DB for project_id=%s", project_id)
            raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Project update failed")

        # Cleanup old files (best-effort) only after successful DB commit
        cleanup_relative(old_overlay)
        cleanup_relative(old_overlay_thumb)
        cleanup_relative(old_logo)
        cleanup_relative(old_logo_thumb)

        return {
            "success": True,
            "message": "Project updated successfully.",
            "project_id": project.id,
        }

    @staticmethod
    def delete_project(session: Session, project_id: int):
        project = ProjectRepository.get_project(session, project_id)
        if not project:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERR_NOT_FOUND)

        # Soft-delete the project row and related images via repository
        ProjectRepository.soft_delete_project(session, project)

        # Attempt to delete final image files referenced by product images (best-effort)
        images = ProductImageRepository.list_by_project(session, project_id)
        for img in images:
            try:
                # convert DB path -> absolute and delete
                abs_path = abs_from_db(img.final_image_path)
                if abs_path:
                    safe_delete(abs_path)
            except Exception:
                logger.exception("Failed to delete final image file: %s", img.final_image_path)

        ProductImageRepository.delete_by_project(session, project_id)
