//! Session GraphQL mutations
//!
//! Mutations for managing service and project sessions, including
//! session lifecycle, task completions, notes, and media uploads.

use std::sync::Arc;

use async_graphql::{Context, InputObject, Object, Result, Upload};
use bytes::Bytes;
use chrono::{Datelike, NaiveDate, Utc};
use tempfile::NamedTempFile;
use uuid::Uuid;

use crate::auth::UserContext;
use crate::db::Database;
use crate::models::EventType;
use crate::services::{EventPublisher, JobQueue};
use crate::graphql::types::{
    ProjectSessionImageType, ProjectSessionNoteType, ProjectSessionType, ProjectSessionVideoType,
    ProjectTaskCompletionType, ServiceSessionImageType, ServiceSessionNoteType,
    ServiceSessionType, ServiceSessionVideoType, ServiceTaskCompletionType,
};
use crate::models::{
    ProjectSession, ProjectSessionImage, ProjectSessionNote, ProjectSessionVideo,
    ServiceSession, ServiceSessionImage, ServiceSessionNote,
    ServiceSessionVideo, WorkStatus,
};
use crate::services::{process_image, S3Service};

// ==================== INPUT TYPES ====================

/// Input for opening a service session
#[derive(InputObject)]
pub struct OpenServiceSessionInput {
    pub service_id: Uuid,
}

/// Input for closing a service session
#[derive(InputObject)]
pub struct CloseServiceSessionInput {
    pub service_id: Uuid,
    /// Task IDs to mark as completed when closing the session
    pub task_ids: Vec<Uuid>,
}

/// Input for opening a project session
#[derive(InputObject)]
pub struct OpenProjectSessionInput {
    pub project_id: Uuid,
}

/// Input for closing a project session
#[derive(InputObject)]
pub struct CloseProjectSessionInput {
    pub project_id: Uuid,
    /// Task IDs to mark as completed when closing the session
    pub task_ids: Vec<Uuid>,
}

/// Input for creating a session note
#[derive(InputObject)]
pub struct CreateSessionNoteInput {
    pub session_id: Uuid,
    pub content: String,
    #[graphql(default = false)]
    pub internal: bool,
}

/// Input for updating a session note
#[derive(InputObject)]
pub struct UpdateSessionNoteInput {
    pub content: Option<String>,
    pub internal: Option<bool>,
}

/// Input for updating session media
#[derive(InputObject)]
pub struct UpdateSessionMediaInput {
    pub title: Option<String>,
    pub notes: Option<String>,
    pub internal: Option<bool>,
}

// ==================== MUTATION IMPLEMENTATION ====================

#[derive(Default)]
pub struct SessionMutation;

#[Object]
impl SessionMutation {
    // ==================== SERVICE SESSION LIFECYCLE ====================

    /// Open a new service session
    ///
    /// - Validates service status is SCHEDULED
    /// - Finds active scope for the service's account address
    /// - Creates session with start=now, created_by=actor
    /// - Sets service status to IN_PROGRESS
    async fn open_service_session(
        &self,
        ctx: &Context<'_>,
        input: OpenServiceSessionInput,
    ) -> Result<ServiceSessionType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        // Get the service and validate status
        let service: (Uuid, Uuid, Uuid, WorkStatus, NaiveDate) = sqlx::query_as(
            r#"
            SELECT s.account_id, s.account_address_id, a.customer_id, s.status, s.date
            FROM services s
            JOIN accounts a ON a.id = s.account_id
            WHERE s.id = $1
            "#,
        )
        .bind(input.service_id)
        .fetch_optional(&*pool)
        .await?
        .ok_or_else(|| async_graphql::Error::new("Service not found"))?;

        let (account_id, account_address_id, customer_id, status, service_date) = service;

        if status != WorkStatus::Scheduled {
            return Err(async_graphql::Error::new(format!(
                "Service must be in SCHEDULED status to open a session (current: {:?})",
                status
            )));
        }

        // Find active scope for this account address
        let scope_id: Option<Uuid> = sqlx::query_scalar(
            r#"
            SELECT id FROM service_scopes
            WHERE account_address_id = $1 AND is_active = true
            LIMIT 1
            "#,
        )
        .bind(account_address_id)
        .fetch_optional(&*pool)
        .await?;

        // Create the session
        let now = Utc::now();
        let session: ServiceSession = sqlx::query_as(
            r#"
            INSERT INTO service_sessions (
                id, created_at, updated_at, service_id, account_id, account_address_id,
                customer_id, scope_id, start, created_by_id, date
            )
            VALUES (
                gen_random_uuid(), $1, $1, $2, $3, $4, $5, $6, $1, $7, $8
            )
            RETURNING id, created_at, updated_at, service_id, account_id, account_address_id,
                      customer_id, scope_id, start, "end", created_by_id, closed_by_id, date
            "#,
        )
        .bind(now)
        .bind(input.service_id)
        .bind(account_id)
        .bind(account_address_id)
        .bind(customer_id)
        .bind(scope_id)
        .bind(user.user_id)
        .bind(service_date)
        .fetch_one(&*pool)
        .await?;

        // Update service status to IN_PROGRESS
        sqlx::query(
            r#"
            UPDATE services SET status = 'IN_PROGRESS', updated_at = NOW()
            WHERE id = $1
            "#,
        )
        .bind(input.service_id)
        .execute(&*pool)
        .await?;

        // Publish event for audit trail
        if let Ok(job_queue) = ctx.data::<JobQueue>() {
            let _ = EventPublisher::publish_and_queue(
                &pool,
                job_queue,
                EventType::ServiceSessionStarted,
                "service_session",
                session.base.id,
                Some(("team_profile", user.user_id)),
                None,
            )
            .await;
        }

        Ok(ServiceSessionType::from(session))
    }

    /// Close a service session
    ///
    /// - Validates service status is IN_PROGRESS
    /// - Processes task completions (deduplicate, validate scope ownership)
    /// - Sets session end=now, closed_by=actor
    /// - Sets service status to COMPLETED
    async fn close_service_session(
        &self,
        ctx: &Context<'_>,
        input: CloseServiceSessionInput,
    ) -> Result<ServiceSessionType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        // Get the active session
        let session: ServiceSession = sqlx::query_as(
            r#"
            SELECT id, created_at, updated_at, service_id, account_id, account_address_id,
                   customer_id, scope_id, start, "end", created_by_id, closed_by_id, date
            FROM service_sessions
            WHERE service_id = $1 AND "end" IS NULL
            "#,
        )
        .bind(input.service_id)
        .fetch_optional(&*pool)
        .await?
        .ok_or_else(|| async_graphql::Error::new("No active session found for this service"))?;

        // Verify service is IN_PROGRESS
        let status: WorkStatus = sqlx::query_scalar(
            "SELECT status FROM services WHERE id = $1",
        )
        .bind(input.service_id)
        .fetch_one(&*pool)
        .await?;

        if status != WorkStatus::InProgress {
            return Err(async_graphql::Error::new(format!(
                "Service must be IN_PROGRESS to close a session (current: {:?})",
                status
            )));
        }

        // Process task completions
        let now = Utc::now();
        let year = session.date.year();
        let month = session.date.month() as i32;

        for task_id in input.task_ids {
            // Skip if already completed for this service
            let existing: Option<Uuid> = sqlx::query_scalar(
                "SELECT id FROM service_task_completions WHERE service_id = $1 AND task_id = $2",
            )
            .bind(input.service_id)
            .bind(task_id)
            .fetch_optional(&*pool)
            .await?;

            let completion_id = if let Some(id) = existing {
                id
            } else {
                // Create new completion
                sqlx::query_scalar(
                    r#"
                    INSERT INTO service_task_completions (
                        id, created_at, updated_at, service_id, task_id, account_address_id,
                        completed_by_id, completed_at, year, month
                    )
                    VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, $5, $1, $6, $7)
                    RETURNING id
                    "#,
                )
                .bind(now)
                .bind(input.service_id)
                .bind(task_id)
                .bind(session.account_address_id)
                .bind(user.user_id)
                .bind(year)
                .bind(month)
                .fetch_one(&*pool)
                .await?
            };

            // Link to session via junction table (ignore if already linked)
            sqlx::query(
                r#"
                INSERT INTO service_session_completed_tasks (id, created_at, session_id, task_completion_id)
                VALUES (gen_random_uuid(), $1, $2, $3)
                ON CONFLICT (session_id, task_completion_id) DO NOTHING
                "#,
            )
            .bind(now)
            .bind(session.base.id)
            .bind(completion_id)
            .execute(&*pool)
            .await?;
        }

        // Close the session
        let closed_session: ServiceSession = sqlx::query_as(
            r#"
            UPDATE service_sessions
            SET "end" = $2, closed_by_id = $3, updated_at = $2
            WHERE id = $1
            RETURNING id, created_at, updated_at, service_id, account_id, account_address_id,
                      customer_id, scope_id, start, "end", created_by_id, closed_by_id, date
            "#,
        )
        .bind(session.base.id)
        .bind(now)
        .bind(user.user_id)
        .fetch_one(&*pool)
        .await?;

        // Update service status to COMPLETED
        sqlx::query(
            r#"
            UPDATE services SET status = 'COMPLETED', updated_at = NOW()
            WHERE id = $1
            "#,
        )
        .bind(input.service_id)
        .execute(&*pool)
        .await?;

        // Publish event for audit trail
        if let Ok(job_queue) = ctx.data::<JobQueue>() {
            let _ = EventPublisher::publish_and_queue(
                &pool,
                job_queue,
                EventType::ServiceSessionEnded,
                "service_session",
                closed_session.base.id,
                Some(("team_profile", user.user_id)),
                None,
            )
            .await;
        }

        Ok(ServiceSessionType::from(closed_session))
    }

    /// Revert a service session (admin only)
    ///
    /// - Deletes task completions linked to session
    /// - Deletes the session
    /// - Sets service status back to SCHEDULED
    async fn revert_service_session(
        &self,
        ctx: &Context<'_>,
        service_id: Uuid,
    ) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        // Find the most recent session
        let session_id: Option<Uuid> = sqlx::query_scalar(
            r#"
            SELECT id FROM service_sessions
            WHERE service_id = $1
            ORDER BY start DESC
            LIMIT 1
            "#,
        )
        .bind(service_id)
        .fetch_optional(&*pool)
        .await?;

        let Some(session_id) = session_id else {
            return Err(async_graphql::Error::new("No session found to revert"));
        };

        // Get all task completion IDs linked to this session
        let completion_ids: Vec<Uuid> = sqlx::query_scalar(
            "SELECT task_completion_id FROM service_session_completed_tasks WHERE session_id = $1",
        )
        .bind(session_id)
        .fetch_all(&*pool)
        .await?;

        // Delete from junction table first
        sqlx::query("DELETE FROM service_session_completed_tasks WHERE session_id = $1")
            .bind(session_id)
            .execute(&*pool)
            .await?;

        // Delete the task completions
        for completion_id in completion_ids {
            sqlx::query("DELETE FROM service_task_completions WHERE id = $1")
                .bind(completion_id)
                .execute(&*pool)
                .await?;
        }

        // Delete notes
        sqlx::query("DELETE FROM service_session_notes WHERE session_id = $1")
            .bind(session_id)
            .execute(&*pool)
            .await?;

        // Delete images (S3 cleanup would happen separately)
        sqlx::query("DELETE FROM service_session_images WHERE session_id = $1")
            .bind(session_id)
            .execute(&*pool)
            .await?;

        // Delete videos
        sqlx::query("DELETE FROM service_session_videos WHERE session_id = $1")
            .bind(session_id)
            .execute(&*pool)
            .await?;

        // Delete the session
        sqlx::query("DELETE FROM service_sessions WHERE id = $1")
            .bind(session_id)
            .execute(&*pool)
            .await?;

        // Set service status back to SCHEDULED
        sqlx::query(
            r#"
            UPDATE services SET status = 'SCHEDULED', updated_at = NOW()
            WHERE id = $1
            "#,
        )
        .bind(service_id)
        .execute(&*pool)
        .await?;

        // Publish event for audit trail
        if let Ok(job_queue) = ctx.data::<JobQueue>() {
            let _ = EventPublisher::publish_and_queue(
                &pool,
                job_queue,
                EventType::ServiceSessionReverted,
                "service",
                service_id,
                Some(("team_profile", user.user_id)),
                None,
            )
            .await;
        }

        Ok(true)
    }

    // ==================== PROJECT SESSION LIFECYCLE ====================

    /// Open a new project session
    async fn open_project_session(
        &self,
        ctx: &Context<'_>,
        input: OpenProjectSessionInput,
    ) -> Result<ProjectSessionType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        // Get the project and validate status
        let project: (Uuid, Option<Uuid>, Option<Uuid>, WorkStatus, NaiveDate) = sqlx::query_as(
            r#"
            SELECT p.customer_id, aa.account_id, p.account_address_id, p.status, p.date
            FROM projects p
            LEFT JOIN account_addresses aa ON aa.id = p.account_address_id
            WHERE p.id = $1
            "#,
        )
        .bind(input.project_id)
        .fetch_optional(&*pool)
        .await?
        .ok_or_else(|| async_graphql::Error::new("Project not found"))?;

        let (customer_id, account_id, account_address_id, status, project_date) = project;

        if status != WorkStatus::Scheduled {
            return Err(async_graphql::Error::new(format!(
                "Project must be in SCHEDULED status to open a session (current: {:?})",
                status
            )));
        }

        // Find active scope for this project
        let scope_id: Option<Uuid> = sqlx::query_scalar(
            r#"
            SELECT id FROM project_scopes
            WHERE project_id = $1 AND is_active = true
            LIMIT 1
            "#,
        )
        .bind(input.project_id)
        .fetch_optional(&*pool)
        .await?;

        // Create the session
        let now = Utc::now();
        let session: ProjectSession = sqlx::query_as(
            r#"
            INSERT INTO project_sessions (
                id, created_at, updated_at, project_id, account_id, account_address_id,
                customer_id, scope_id, start, created_by_id, date
            )
            VALUES (
                gen_random_uuid(), $1, $1, $2, $3, $4, $5, $6, $1, $7, $8
            )
            RETURNING id, created_at, updated_at, project_id, account_id, account_address_id,
                      customer_id, scope_id, start, "end", created_by_id, closed_by_id, date
            "#,
        )
        .bind(now)
        .bind(input.project_id)
        .bind(account_id)
        .bind(account_address_id)
        .bind(customer_id)
        .bind(scope_id)
        .bind(user.user_id)
        .bind(project_date)
        .fetch_one(&*pool)
        .await?;

        // Update project status to IN_PROGRESS
        sqlx::query(
            r#"
            UPDATE projects SET status = 'IN_PROGRESS', updated_at = NOW()
            WHERE id = $1
            "#,
        )
        .bind(input.project_id)
        .execute(&*pool)
        .await?;

        // Publish event for audit trail
        if let Ok(job_queue) = ctx.data::<JobQueue>() {
            let _ = EventPublisher::publish_and_queue(
                &pool,
                job_queue,
                EventType::ProjectSessionStarted,
                "project_session",
                session.base.id,
                Some(("team_profile", user.user_id)),
                None,
            )
            .await;
        }

        Ok(ProjectSessionType::from(session))
    }

    /// Close a project session
    async fn close_project_session(
        &self,
        ctx: &Context<'_>,
        input: CloseProjectSessionInput,
    ) -> Result<ProjectSessionType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        // Get the active session
        let session: ProjectSession = sqlx::query_as(
            r#"
            SELECT id, created_at, updated_at, project_id, account_id, account_address_id,
                   customer_id, scope_id, start, "end", created_by_id, closed_by_id, date
            FROM project_sessions
            WHERE project_id = $1 AND "end" IS NULL
            "#,
        )
        .bind(input.project_id)
        .fetch_optional(&*pool)
        .await?
        .ok_or_else(|| async_graphql::Error::new("No active session found for this project"))?;

        // Verify project is IN_PROGRESS
        let status: WorkStatus = sqlx::query_scalar(
            "SELECT status FROM projects WHERE id = $1",
        )
        .bind(input.project_id)
        .fetch_one(&*pool)
        .await?;

        if status != WorkStatus::InProgress {
            return Err(async_graphql::Error::new(format!(
                "Project must be IN_PROGRESS to close a session (current: {:?})",
                status
            )));
        }

        // Process task completions
        let now = Utc::now();

        for task_id in input.task_ids {
            // Skip if already completed for this project
            let existing: Option<Uuid> = sqlx::query_scalar(
                "SELECT id FROM project_task_completions WHERE project_id = $1 AND task_id = $2",
            )
            .bind(input.project_id)
            .bind(task_id)
            .fetch_optional(&*pool)
            .await?;

            let completion_id = if let Some(id) = existing {
                id
            } else {
                // Create new completion
                sqlx::query_scalar(
                    r#"
                    INSERT INTO project_task_completions (
                        id, created_at, updated_at, project_id, task_id, account_id, account_address_id,
                        completed_by_id, completed_at
                    )
                    VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, $5, $6, $1)
                    RETURNING id
                    "#,
                )
                .bind(now)
                .bind(input.project_id)
                .bind(task_id)
                .bind(session.account_id)
                .bind(session.account_address_id)
                .bind(user.user_id)
                .fetch_one(&*pool)
                .await?
            };

            // Link to session via junction table (ignore if already linked)
            sqlx::query(
                r#"
                INSERT INTO project_session_completed_tasks (id, created_at, session_id, task_completion_id)
                VALUES (gen_random_uuid(), $1, $2, $3)
                ON CONFLICT (session_id, task_completion_id) DO NOTHING
                "#,
            )
            .bind(now)
            .bind(session.base.id)
            .bind(completion_id)
            .execute(&*pool)
            .await?;
        }

        // Close the session
        let closed_session: ProjectSession = sqlx::query_as(
            r#"
            UPDATE project_sessions
            SET "end" = $2, closed_by_id = $3, updated_at = $2
            WHERE id = $1
            RETURNING id, created_at, updated_at, project_id, account_id, account_address_id,
                      customer_id, scope_id, start, "end", created_by_id, closed_by_id, date
            "#,
        )
        .bind(session.base.id)
        .bind(now)
        .bind(user.user_id)
        .fetch_one(&*pool)
        .await?;

        // Update project status to COMPLETED
        sqlx::query(
            r#"
            UPDATE projects SET status = 'COMPLETED', updated_at = NOW()
            WHERE id = $1
            "#,
        )
        .bind(input.project_id)
        .execute(&*pool)
        .await?;

        // Publish event for audit trail
        if let Ok(job_queue) = ctx.data::<JobQueue>() {
            let _ = EventPublisher::publish_and_queue(
                &pool,
                job_queue,
                EventType::ProjectSessionEnded,
                "project_session",
                closed_session.base.id,
                Some(("team_profile", user.user_id)),
                None,
            )
            .await;
        }

        Ok(ProjectSessionType::from(closed_session))
    }

    /// Revert a project session (admin only)
    async fn revert_project_session(
        &self,
        ctx: &Context<'_>,
        project_id: Uuid,
    ) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        // Find the most recent session
        let session_id: Option<Uuid> = sqlx::query_scalar(
            r#"
            SELECT id FROM project_sessions
            WHERE project_id = $1
            ORDER BY start DESC
            LIMIT 1
            "#,
        )
        .bind(project_id)
        .fetch_optional(&*pool)
        .await?;

        let Some(session_id) = session_id else {
            return Err(async_graphql::Error::new("No session found to revert"));
        };

        // Get all task completion IDs linked to this session
        let completion_ids: Vec<Uuid> = sqlx::query_scalar(
            "SELECT task_completion_id FROM project_session_completed_tasks WHERE session_id = $1",
        )
        .bind(session_id)
        .fetch_all(&*pool)
        .await?;

        // Delete from junction table first
        sqlx::query("DELETE FROM project_session_completed_tasks WHERE session_id = $1")
            .bind(session_id)
            .execute(&*pool)
            .await?;

        // Delete the task completions
        for completion_id in completion_ids {
            sqlx::query("DELETE FROM project_task_completions WHERE id = $1")
                .bind(completion_id)
                .execute(&*pool)
                .await?;
        }

        // Delete notes
        sqlx::query("DELETE FROM project_session_notes WHERE session_id = $1")
            .bind(session_id)
            .execute(&*pool)
            .await?;

        // Delete images
        sqlx::query("DELETE FROM project_session_images WHERE session_id = $1")
            .bind(session_id)
            .execute(&*pool)
            .await?;

        // Delete videos
        sqlx::query("DELETE FROM project_session_videos WHERE session_id = $1")
            .bind(session_id)
            .execute(&*pool)
            .await?;

        // Delete the session
        sqlx::query("DELETE FROM project_sessions WHERE id = $1")
            .bind(session_id)
            .execute(&*pool)
            .await?;

        // Set project status back to SCHEDULED
        sqlx::query(
            r#"
            UPDATE projects SET status = 'SCHEDULED', updated_at = NOW()
            WHERE id = $1
            "#,
        )
        .bind(project_id)
        .execute(&*pool)
        .await?;

        // Publish event for audit trail
        if let Ok(job_queue) = ctx.data::<JobQueue>() {
            let _ = EventPublisher::publish_and_queue(
                &pool,
                job_queue,
                EventType::ProjectSessionReverted,
                "project",
                project_id,
                Some(("team_profile", user.user_id)),
                None,
            )
            .await;
        }

        Ok(true)
    }

    // ==================== TASK COMPLETIONS ====================

    /// Add a task completion to the active service session
    async fn add_service_task_completion(
        &self,
        ctx: &Context<'_>,
        service_id: Uuid,
        task_id: Uuid,
        notes: Option<String>,
    ) -> Result<ServiceTaskCompletionType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        // Get the active session with account_address_id and date
        let session: (Uuid, Uuid, NaiveDate) = sqlx::query_as(
            r#"
            SELECT id, account_address_id, date FROM service_sessions
            WHERE service_id = $1 AND "end" IS NULL
            "#,
        )
        .bind(service_id)
        .fetch_optional(&*pool)
        .await?
        .ok_or_else(|| async_graphql::Error::new("No active session found"))?;

        let (session_id, account_address_id, date) = session;
        let year = date.year();
        let month = date.month() as i32;
        let now = Utc::now();

        // Insert task completion (or update if already exists for this service+task)
        let completion_id: Uuid = sqlx::query_scalar(
            r#"
            INSERT INTO service_task_completions (
                id, created_at, updated_at, service_id, task_id, account_address_id,
                completed_by_id, completed_at, year, month, notes
            )
            VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, $5, $1, $6, $7, $8)
            ON CONFLICT (service_id, task_id) DO UPDATE SET
                notes = COALESCE(EXCLUDED.notes, service_task_completions.notes),
                updated_at = $1
            RETURNING id
            "#,
        )
        .bind(now)
        .bind(service_id)
        .bind(task_id)
        .bind(account_address_id)
        .bind(user.user_id)
        .bind(year)
        .bind(month)
        .bind(&notes)
        .fetch_one(&*pool)
        .await?;

        // Link to session via junction table (ignore if already linked)
        sqlx::query(
            r#"
            INSERT INTO service_session_completed_tasks (id, created_at, session_id, task_completion_id)
            VALUES (gen_random_uuid(), $1, $2, $3)
            ON CONFLICT (session_id, task_completion_id) DO NOTHING
            "#,
        )
        .bind(now)
        .bind(session_id)
        .bind(completion_id)
        .execute(&*pool)
        .await?;

        Ok(ServiceTaskCompletionType {
            id: completion_id,
            session_id,
            task_id,
            completed_by_id: user.user_id,
            completed_at: now,
            notes,
        })
    }

    /// Remove a task completion from the active service session
    async fn remove_service_task_completion(
        &self,
        ctx: &Context<'_>,
        service_id: Uuid,
        task_id: Uuid,
    ) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        // Get the active session
        let session_id: Uuid = sqlx::query_scalar(
            r#"
            SELECT id FROM service_sessions
            WHERE service_id = $1 AND "end" IS NULL
            "#,
        )
        .bind(service_id)
        .fetch_optional(&*pool)
        .await?
        .ok_or_else(|| async_graphql::Error::new("No active session found"))?;

        // Find the completion for this service+task
        let completion_id: Option<Uuid> = sqlx::query_scalar(
            "SELECT id FROM service_task_completions WHERE service_id = $1 AND task_id = $2",
        )
        .bind(service_id)
        .bind(task_id)
        .fetch_optional(&*pool)
        .await?;

        let Some(completion_id) = completion_id else {
            // Nothing to remove
            return Ok(false);
        };

        // Remove from junction table
        sqlx::query(
            "DELETE FROM service_session_completed_tasks WHERE session_id = $1 AND task_completion_id = $2",
        )
        .bind(session_id)
        .bind(completion_id)
        .execute(&*pool)
        .await?;

        // Delete the completion record
        let result = sqlx::query("DELETE FROM service_task_completions WHERE id = $1")
            .bind(completion_id)
            .execute(&*pool)
            .await?;

        Ok(result.rows_affected() > 0)
    }

    /// Add a task completion to the active project session
    async fn add_project_task_completion(
        &self,
        ctx: &Context<'_>,
        project_id: Uuid,
        task_id: Uuid,
        notes: Option<String>,
    ) -> Result<ProjectTaskCompletionType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        // Get the active session with account_id and account_address_id
        let session: (Uuid, Option<Uuid>, Option<Uuid>) = sqlx::query_as(
            r#"
            SELECT id, account_id, account_address_id FROM project_sessions
            WHERE project_id = $1 AND "end" IS NULL
            "#,
        )
        .bind(project_id)
        .fetch_optional(&*pool)
        .await?
        .ok_or_else(|| async_graphql::Error::new("No active session found"))?;

        let (session_id, account_id, account_address_id) = session;
        let now = Utc::now();

        // Insert task completion (or update if already exists for this project+task)
        let completion_id: Uuid = sqlx::query_scalar(
            r#"
            INSERT INTO project_task_completions (
                id, created_at, updated_at, project_id, task_id, account_id, account_address_id,
                completed_by_id, completed_at, notes
            )
            VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, $5, $6, $1, $7)
            ON CONFLICT (project_id, task_id) DO UPDATE SET
                notes = COALESCE(EXCLUDED.notes, project_task_completions.notes),
                updated_at = $1
            RETURNING id
            "#,
        )
        .bind(now)
        .bind(project_id)
        .bind(task_id)
        .bind(account_id)
        .bind(account_address_id)
        .bind(user.user_id)
        .bind(&notes)
        .fetch_one(&*pool)
        .await?;

        // Link to session via junction table (ignore if already linked)
        sqlx::query(
            r#"
            INSERT INTO project_session_completed_tasks (id, created_at, session_id, task_completion_id)
            VALUES (gen_random_uuid(), $1, $2, $3)
            ON CONFLICT (session_id, task_completion_id) DO NOTHING
            "#,
        )
        .bind(now)
        .bind(session_id)
        .bind(completion_id)
        .execute(&*pool)
        .await?;

        Ok(ProjectTaskCompletionType {
            id: completion_id,
            session_id,
            task_id,
            completed_by_id: user.user_id,
            completed_at: now,
            notes,
        })
    }

    /// Remove a task completion from the active project session
    async fn remove_project_task_completion(
        &self,
        ctx: &Context<'_>,
        project_id: Uuid,
        task_id: Uuid,
    ) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        // Get the active session
        let session_id: Uuid = sqlx::query_scalar(
            r#"
            SELECT id FROM project_sessions
            WHERE project_id = $1 AND "end" IS NULL
            "#,
        )
        .bind(project_id)
        .fetch_optional(&*pool)
        .await?
        .ok_or_else(|| async_graphql::Error::new("No active session found"))?;

        // Find the completion for this project+task
        let completion_id: Option<Uuid> = sqlx::query_scalar(
            "SELECT id FROM project_task_completions WHERE project_id = $1 AND task_id = $2",
        )
        .bind(project_id)
        .bind(task_id)
        .fetch_optional(&*pool)
        .await?;

        let Some(completion_id) = completion_id else {
            // Nothing to remove
            return Ok(false);
        };

        // Remove from junction table
        sqlx::query(
            "DELETE FROM project_session_completed_tasks WHERE session_id = $1 AND task_completion_id = $2",
        )
        .bind(session_id)
        .bind(completion_id)
        .execute(&*pool)
        .await?;

        // Delete the completion record
        let result = sqlx::query("DELETE FROM project_task_completions WHERE id = $1")
            .bind(completion_id)
            .execute(&*pool)
            .await?;

        Ok(result.rows_affected() > 0)
    }

    // ==================== SERVICE SESSION NOTES ====================

    /// Create a note for a service session
    async fn create_service_session_note(
        &self,
        ctx: &Context<'_>,
        input: CreateSessionNoteInput,
    ) -> Result<ServiceSessionNoteType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        let now = Utc::now();
        let note: ServiceSessionNote = sqlx::query_as(
            r#"
            INSERT INTO service_session_notes (
                id, created_at, updated_at, session_id, content, author_id, internal
            )
            VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, $5)
            RETURNING id, created_at, updated_at, session_id, content, author_id, internal
            "#,
        )
        .bind(now)
        .bind(input.session_id)
        .bind(&input.content)
        .bind(user.user_id)
        .bind(input.internal)
        .fetch_one(&*pool)
        .await?;

        Ok(ServiceSessionNoteType::from(note))
    }

    /// Update a service session note
    async fn update_service_session_note(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
        input: UpdateSessionNoteInput,
    ) -> Result<ServiceSessionNoteType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        let note: ServiceSessionNote = sqlx::query_as(
            r#"
            UPDATE service_session_notes
            SET
                content = COALESCE($2, content),
                internal = COALESCE($3, internal),
                updated_at = NOW()
            WHERE id = $1
            RETURNING id, created_at, updated_at, session_id, content, author_id, internal
            "#,
        )
        .bind(id)
        .bind(&input.content)
        .bind(input.internal)
        .fetch_one(&*pool)
        .await?;

        Ok(ServiceSessionNoteType::from(note))
    }

    /// Delete a service session note
    async fn delete_service_session_note(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
    ) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        let result = sqlx::query("DELETE FROM service_session_notes WHERE id = $1")
            .bind(id)
            .execute(&*pool)
            .await?;

        Ok(result.rows_affected() > 0)
    }

    // ==================== PROJECT SESSION NOTES ====================

    /// Create a note for a project session
    async fn create_project_session_note(
        &self,
        ctx: &Context<'_>,
        input: CreateSessionNoteInput,
    ) -> Result<ProjectSessionNoteType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        let now = Utc::now();
        let note: ProjectSessionNote = sqlx::query_as(
            r#"
            INSERT INTO project_session_notes (
                id, created_at, updated_at, session_id, content, author_id, internal
            )
            VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, $5)
            RETURNING id, created_at, updated_at, session_id, content, author_id, internal
            "#,
        )
        .bind(now)
        .bind(input.session_id)
        .bind(&input.content)
        .bind(user.user_id)
        .bind(input.internal)
        .fetch_one(&*pool)
        .await?;

        Ok(ProjectSessionNoteType::from(note))
    }

    /// Update a project session note
    async fn update_project_session_note(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
        input: UpdateSessionNoteInput,
    ) -> Result<ProjectSessionNoteType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        let note: ProjectSessionNote = sqlx::query_as(
            r#"
            UPDATE project_session_notes
            SET
                content = COALESCE($2, content),
                internal = COALESCE($3, internal),
                updated_at = NOW()
            WHERE id = $1
            RETURNING id, created_at, updated_at, session_id, content, author_id, internal
            "#,
        )
        .bind(id)
        .bind(&input.content)
        .bind(input.internal)
        .fetch_one(&*pool)
        .await?;

        Ok(ProjectSessionNoteType::from(note))
    }

    /// Delete a project session note
    async fn delete_project_session_note(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
    ) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        let result = sqlx::query("DELETE FROM project_session_notes WHERE id = $1")
            .bind(id)
            .execute(&*pool)
            .await?;

        Ok(result.rows_affected() > 0)
    }

    // ==================== SERVICE SESSION IMAGES ====================

    /// Upload an image to a service session
    ///
    /// Handles HEIC/HEIF conversion to JPEG, generates thumbnails,
    /// and uploads both to S3.
    async fn upload_service_session_image(
        &self,
        ctx: &Context<'_>,
        session_id: Uuid,
        file: Upload,
        title: Option<String>,
        notes: Option<String>,
        #[graphql(default = false)] internal: bool,
    ) -> Result<ServiceSessionImageType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;
        let s3 = ctx.data::<Arc<S3Service>>()?;

        // Read the uploaded file
        let upload = file.value(ctx)?;
        let filename = upload.filename.clone();
        let mut data = Vec::new();
        use std::io::Read;
        let mut content = upload.into_read();
        content.read_to_end(&mut data)?;

        // Use filename as default title if not provided
        let title = title.unwrap_or_else(|| filename.clone());
        let notes = notes.unwrap_or_default();

        // Process the image (handles HEIC conversion, gets dimensions)
        let processed = process_image(&data, &filename)
            .map_err(|e| async_graphql::Error::new(format!("Image processing failed: {}", e)))?;

        // Generate thumbnail
        let thumbnail = crate::services::generate_thumbnail(&processed.data, 320)
            .map_err(|e| async_graphql::Error::new(format!("Thumbnail generation failed: {}", e)))?;

        // Generate unique paths
        let image_id = Uuid::new_v4();
        let image_path = format!("sessions/service/{}/{}.jpg", session_id, image_id);
        let thumbnail_path = format!("sessions/service/{}/{}_thumb.jpg", session_id, image_id);

        // Upload to S3
        s3.upload_file(&image_path, Bytes::from(processed.data.to_vec()), &processed.content_type)
            .await
            .map_err(|e| async_graphql::Error::new(format!("Failed to upload image: {}", e)))?;

        s3.upload_file(&thumbnail_path, Bytes::from(thumbnail), "image/jpeg")
            .await
            .map_err(|e| async_graphql::Error::new(format!("Failed to upload thumbnail: {}", e)))?;

        // Create database record
        let now = Utc::now();
        let image: ServiceSessionImage = sqlx::query_as(
            r#"
            INSERT INTO service_session_images (
                id, created_at, updated_at, session_id, title, image, thumbnail,
                content_type, width, height, uploaded_by_team_profile_id, notes, internal
            )
            VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
            RETURNING id, created_at, updated_at, session_id, title, image, thumbnail,
                      content_type, width, height, uploaded_by_team_profile_id AS uploaded_by_id, notes, internal
            "#,
        )
        .bind(now)
        .bind(session_id)
        .bind(&title)
        .bind(&image_path)
        .bind(&thumbnail_path)
        .bind(&processed.content_type)
        .bind(processed.width as i32)
        .bind(processed.height as i32)
        .bind(user.user_id)
        .bind(&notes)
        .bind(internal)
        .fetch_one(&*pool)
        .await?;

        Ok(ServiceSessionImageType::from(image))
    }

    /// Update a service session image metadata
    async fn update_service_session_image(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
        input: UpdateSessionMediaInput,
    ) -> Result<ServiceSessionImageType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        let image: ServiceSessionImage = sqlx::query_as(
            r#"
            UPDATE service_session_images
            SET
                title = COALESCE($2, title),
                notes = COALESCE($3, notes),
                internal = COALESCE($4, internal),
                updated_at = NOW()
            WHERE id = $1
            RETURNING id, created_at, updated_at, session_id, title, image, thumbnail,
                      content_type, width, height, uploaded_by_team_profile_id AS uploaded_by_id, notes, internal
            "#,
        )
        .bind(id)
        .bind(&input.title)
        .bind(&input.notes)
        .bind(input.internal)
        .fetch_one(&*pool)
        .await?;

        Ok(ServiceSessionImageType::from(image))
    }

    /// Delete a service session image
    async fn delete_service_session_image(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
    ) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let s3 = ctx.data::<Arc<S3Service>>()?;

        // Get paths before deleting
        let paths: Option<(String, Option<String>)> = sqlx::query_as(
            "SELECT image, thumbnail FROM service_session_images WHERE id = $1",
        )
        .bind(id)
        .fetch_optional(&*pool)
        .await?;

        if let Some((image_path, thumbnail_path)) = paths {
            // Delete from S3
            let _ = s3.delete_file(&image_path).await;
            if let Some(thumb) = thumbnail_path {
                let _ = s3.delete_file(&thumb).await;
            }
        }

        let result = sqlx::query("DELETE FROM service_session_images WHERE id = $1")
            .bind(id)
            .execute(&*pool)
            .await?;

        Ok(result.rows_affected() > 0)
    }

    // ==================== PROJECT SESSION IMAGES ====================

    /// Upload an image to a project session
    async fn upload_project_session_image(
        &self,
        ctx: &Context<'_>,
        session_id: Uuid,
        file: Upload,
        title: Option<String>,
        notes: Option<String>,
        #[graphql(default = false)] internal: bool,
    ) -> Result<ProjectSessionImageType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;
        let s3 = ctx.data::<Arc<S3Service>>()?;

        // Read the uploaded file
        let upload = file.value(ctx)?;
        let filename = upload.filename.clone();
        let mut data = Vec::new();
        use std::io::Read;
        let mut content = upload.into_read();
        content.read_to_end(&mut data)?;

        // Use filename as default title if not provided
        let title = title.unwrap_or_else(|| filename.clone());
        let notes = notes.unwrap_or_default();

        // Process the image
        let processed = process_image(&data, &filename)
            .map_err(|e| async_graphql::Error::new(format!("Image processing failed: {}", e)))?;

        // Generate thumbnail
        let thumbnail = crate::services::generate_thumbnail(&processed.data, 320)
            .map_err(|e| async_graphql::Error::new(format!("Thumbnail generation failed: {}", e)))?;

        // Generate unique paths
        let image_id = Uuid::new_v4();
        let image_path = format!("sessions/project/{}/{}.jpg", session_id, image_id);
        let thumbnail_path = format!("sessions/project/{}/{}_thumb.jpg", session_id, image_id);

        // Upload to S3
        s3.upload_file(&image_path, Bytes::from(processed.data.to_vec()), &processed.content_type)
            .await
            .map_err(|e| async_graphql::Error::new(format!("Failed to upload image: {}", e)))?;

        s3.upload_file(&thumbnail_path, Bytes::from(thumbnail), "image/jpeg")
            .await
            .map_err(|e| async_graphql::Error::new(format!("Failed to upload thumbnail: {}", e)))?;

        // Create database record
        let now = Utc::now();
        let image: ProjectSessionImage = sqlx::query_as(
            r#"
            INSERT INTO project_session_images (
                id, created_at, updated_at, session_id, title, image, thumbnail,
                content_type, width, height, uploaded_by_team_profile_id, notes, internal
            )
            VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
            RETURNING id, created_at, updated_at, session_id, title, image, thumbnail,
                      content_type, width, height, uploaded_by_team_profile_id AS uploaded_by_id, notes, internal
            "#,
        )
        .bind(now)
        .bind(session_id)
        .bind(&title)
        .bind(&image_path)
        .bind(&thumbnail_path)
        .bind(&processed.content_type)
        .bind(processed.width as i32)
        .bind(processed.height as i32)
        .bind(user.user_id)
        .bind(&notes)
        .bind(internal)
        .fetch_one(&*pool)
        .await?;

        Ok(ProjectSessionImageType::from(image))
    }

    /// Update a project session image metadata
    async fn update_project_session_image(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
        input: UpdateSessionMediaInput,
    ) -> Result<ProjectSessionImageType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        let image: ProjectSessionImage = sqlx::query_as(
            r#"
            UPDATE project_session_images
            SET
                title = COALESCE($2, title),
                notes = COALESCE($3, notes),
                internal = COALESCE($4, internal),
                updated_at = NOW()
            WHERE id = $1
            RETURNING id, created_at, updated_at, session_id, title, image, thumbnail,
                      content_type, width, height, uploaded_by_team_profile_id AS uploaded_by_id, notes, internal
            "#,
        )
        .bind(id)
        .bind(&input.title)
        .bind(&input.notes)
        .bind(input.internal)
        .fetch_one(&*pool)
        .await?;

        Ok(ProjectSessionImageType::from(image))
    }

    /// Delete a project session image
    async fn delete_project_session_image(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
    ) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let s3 = ctx.data::<Arc<S3Service>>()?;

        // Get paths before deleting
        let paths: Option<(String, Option<String>)> = sqlx::query_as(
            "SELECT image, thumbnail FROM project_session_images WHERE id = $1",
        )
        .bind(id)
        .fetch_optional(&*pool)
        .await?;

        if let Some((image_path, thumbnail_path)) = paths {
            let _ = s3.delete_file(&image_path).await;
            if let Some(thumb) = thumbnail_path {
                let _ = s3.delete_file(&thumb).await;
            }
        }

        let result = sqlx::query("DELETE FROM project_session_images WHERE id = $1")
            .bind(id)
            .execute(&*pool)
            .await?;

        Ok(result.rows_affected() > 0)
    }

    // ==================== SERVICE SESSION VIDEOS ====================

    /// Upload a video to a service session
    ///
    /// Extracts metadata using ffprobe, generates thumbnail using ffmpeg,
    /// and uploads both to S3.
    async fn upload_service_session_video(
        &self,
        ctx: &Context<'_>,
        session_id: Uuid,
        file: Upload,
        title: Option<String>,
        notes: Option<String>,
        #[graphql(default = false)] internal: bool,
    ) -> Result<ServiceSessionVideoType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;
        let s3 = ctx.data::<Arc<S3Service>>()?;

        // Read the uploaded file
        let upload = file.value(ctx)?;
        let filename = upload.filename.clone();
        let mut data = Vec::new();
        use std::io::Read;
        let mut content = upload.into_read();
        content.read_to_end(&mut data)?;

        // Use filename as default title if not provided
        let title = title.unwrap_or_else(|| filename.clone());
        let notes = notes.unwrap_or_default();

        // Verify video format
        let content_type = crate::services::verify_video(&data, &filename)
            .map_err(|e| async_graphql::Error::new(format!("Invalid video: {}", e)))?;

        // Write to temp file for ffmpeg processing
        let temp_video = NamedTempFile::new()?;
        std::fs::write(temp_video.path(), &data)?;

        // Extract metadata
        let metadata = crate::services::extract_metadata(temp_video.path())
            .map_err(|e| async_graphql::Error::new(format!("Failed to extract metadata: {}", e)))?;

        // Generate thumbnail at 1 second
        let temp_thumb = NamedTempFile::new()?;
        let thumb_path = temp_thumb.path().with_extension("jpg");
        crate::services::generate_video_thumbnail(temp_video.path(), &thumb_path, 1.0)
            .map_err(|e| async_graphql::Error::new(format!("Failed to generate thumbnail: {}", e)))?;

        let thumbnail_data = std::fs::read(&thumb_path)?;

        // Generate unique paths
        let video_id = Uuid::new_v4();
        let ext = crate::services::video::extension_for_content_type(&content_type);
        let video_path = format!("sessions/service/{}/{}.{}", session_id, video_id, ext);
        let thumbnail_path = format!("sessions/service/{}/{}_thumb.jpg", session_id, video_id);

        // Upload to S3
        s3.upload_file(&video_path, Bytes::from(data.clone()), &content_type)
            .await
            .map_err(|e| async_graphql::Error::new(format!("Failed to upload video: {}", e)))?;

        s3.upload_file(&thumbnail_path, Bytes::from(thumbnail_data), "image/jpeg")
            .await
            .map_err(|e| async_graphql::Error::new(format!("Failed to upload thumbnail: {}", e)))?;

        // Create database record
        let now = Utc::now();
        let video: ServiceSessionVideo = sqlx::query_as(
            r#"
            INSERT INTO service_session_videos (
                id, created_at, updated_at, session_id, title, video, thumbnail,
                content_type, width, height, uploaded_by_team_profile_id, notes, internal,
                duration_seconds, file_size_bytes
            )
            VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
            RETURNING id, created_at, updated_at, session_id, title, video, thumbnail,
                      content_type, width, height, uploaded_by_team_profile_id AS uploaded_by_id, notes, internal,
                      duration_seconds, file_size_bytes
            "#,
        )
        .bind(now)
        .bind(session_id)
        .bind(&title)
        .bind(&video_path)
        .bind(&thumbnail_path)
        .bind(&content_type)
        .bind(metadata.width as i32)
        .bind(metadata.height as i32)
        .bind(user.user_id)
        .bind(&notes)
        .bind(internal)
        .bind(metadata.duration_seconds)
        .bind(data.len() as i64)
        .fetch_one(&*pool)
        .await?;

        Ok(ServiceSessionVideoType::from(video))
    }

    /// Update a service session video metadata
    async fn update_service_session_video(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
        input: UpdateSessionMediaInput,
    ) -> Result<ServiceSessionVideoType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        let video: ServiceSessionVideo = sqlx::query_as(
            r#"
            UPDATE service_session_videos
            SET
                title = COALESCE($2, title),
                notes = COALESCE($3, notes),
                internal = COALESCE($4, internal),
                updated_at = NOW()
            WHERE id = $1
            RETURNING id, created_at, updated_at, session_id, title, video, thumbnail,
                      content_type, width, height, uploaded_by_team_profile_id AS uploaded_by_id, notes, internal,
                      duration_seconds, file_size_bytes
            "#,
        )
        .bind(id)
        .bind(&input.title)
        .bind(&input.notes)
        .bind(input.internal)
        .fetch_one(&*pool)
        .await?;

        Ok(ServiceSessionVideoType::from(video))
    }

    /// Delete a service session video
    async fn delete_service_session_video(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
    ) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let s3 = ctx.data::<Arc<S3Service>>()?;

        // Get paths before deleting
        let paths: Option<(String, Option<String>)> = sqlx::query_as(
            "SELECT video, thumbnail FROM service_session_videos WHERE id = $1",
        )
        .bind(id)
        .fetch_optional(&*pool)
        .await?;

        if let Some((video_path, thumbnail_path)) = paths {
            let _ = s3.delete_file(&video_path).await;
            if let Some(thumb) = thumbnail_path {
                let _ = s3.delete_file(&thumb).await;
            }
        }

        let result = sqlx::query("DELETE FROM service_session_videos WHERE id = $1")
            .bind(id)
            .execute(&*pool)
            .await?;

        Ok(result.rows_affected() > 0)
    }

    // ==================== PROJECT SESSION VIDEOS ====================

    /// Upload a video to a project session
    async fn upload_project_session_video(
        &self,
        ctx: &Context<'_>,
        session_id: Uuid,
        file: Upload,
        title: Option<String>,
        notes: Option<String>,
        #[graphql(default = false)] internal: bool,
    ) -> Result<ProjectSessionVideoType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;
        let s3 = ctx.data::<Arc<S3Service>>()?;

        // Read the uploaded file
        let upload = file.value(ctx)?;
        let filename = upload.filename.clone();
        let mut data = Vec::new();
        use std::io::Read;
        let mut content = upload.into_read();
        content.read_to_end(&mut data)?;

        // Use filename as default title if not provided
        let title = title.unwrap_or_else(|| filename.clone());
        let notes = notes.unwrap_or_default();

        // Verify video format
        let content_type = crate::services::verify_video(&data, &filename)
            .map_err(|e| async_graphql::Error::new(format!("Invalid video: {}", e)))?;

        // Write to temp file for ffmpeg processing
        let temp_video = NamedTempFile::new()?;
        std::fs::write(temp_video.path(), &data)?;

        // Extract metadata
        let metadata = crate::services::extract_metadata(temp_video.path())
            .map_err(|e| async_graphql::Error::new(format!("Failed to extract metadata: {}", e)))?;

        // Generate thumbnail
        let temp_thumb = NamedTempFile::new()?;
        let thumb_path = temp_thumb.path().with_extension("jpg");
        crate::services::generate_video_thumbnail(temp_video.path(), &thumb_path, 1.0)
            .map_err(|e| async_graphql::Error::new(format!("Failed to generate thumbnail: {}", e)))?;

        let thumbnail_data = std::fs::read(&thumb_path)?;

        // Generate unique paths
        let video_id = Uuid::new_v4();
        let ext = crate::services::video::extension_for_content_type(&content_type);
        let video_path = format!("sessions/project/{}/{}.{}", session_id, video_id, ext);
        let thumbnail_path = format!("sessions/project/{}/{}_thumb.jpg", session_id, video_id);

        // Upload to S3
        s3.upload_file(&video_path, Bytes::from(data.clone()), &content_type)
            .await
            .map_err(|e| async_graphql::Error::new(format!("Failed to upload video: {}", e)))?;

        s3.upload_file(&thumbnail_path, Bytes::from(thumbnail_data), "image/jpeg")
            .await
            .map_err(|e| async_graphql::Error::new(format!("Failed to upload thumbnail: {}", e)))?;

        // Create database record
        let now = Utc::now();
        let video: ProjectSessionVideo = sqlx::query_as(
            r#"
            INSERT INTO project_session_videos (
                id, created_at, updated_at, session_id, title, video, thumbnail,
                content_type, width, height, uploaded_by_team_profile_id, notes, internal,
                duration_seconds, file_size_bytes
            )
            VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
            RETURNING id, created_at, updated_at, session_id, title, video, thumbnail,
                      content_type, width, height, uploaded_by_team_profile_id AS uploaded_by_id, notes, internal,
                      duration_seconds, file_size_bytes
            "#,
        )
        .bind(now)
        .bind(session_id)
        .bind(&title)
        .bind(&video_path)
        .bind(&thumbnail_path)
        .bind(&content_type)
        .bind(metadata.width as i32)
        .bind(metadata.height as i32)
        .bind(user.user_id)
        .bind(&notes)
        .bind(internal)
        .bind(metadata.duration_seconds)
        .bind(data.len() as i64)
        .fetch_one(&*pool)
        .await?;

        Ok(ProjectSessionVideoType::from(video))
    }

    /// Update a project session video metadata
    async fn update_project_session_video(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
        input: UpdateSessionMediaInput,
    ) -> Result<ProjectSessionVideoType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        let video: ProjectSessionVideo = sqlx::query_as(
            r#"
            UPDATE project_session_videos
            SET
                title = COALESCE($2, title),
                notes = COALESCE($3, notes),
                internal = COALESCE($4, internal),
                updated_at = NOW()
            WHERE id = $1
            RETURNING id, created_at, updated_at, session_id, title, video, thumbnail,
                      content_type, width, height, uploaded_by_team_profile_id AS uploaded_by_id, notes, internal,
                      duration_seconds, file_size_bytes
            "#,
        )
        .bind(id)
        .bind(&input.title)
        .bind(&input.notes)
        .bind(input.internal)
        .fetch_one(&*pool)
        .await?;

        Ok(ProjectSessionVideoType::from(video))
    }

    /// Delete a project session video
    async fn delete_project_session_video(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
    ) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let s3 = ctx.data::<Arc<S3Service>>()?;

        // Get paths before deleting
        let paths: Option<(String, Option<String>)> = sqlx::query_as(
            "SELECT video, thumbnail FROM project_session_videos WHERE id = $1",
        )
        .bind(id)
        .fetch_optional(&*pool)
        .await?;

        if let Some((video_path, thumbnail_path)) = paths {
            let _ = s3.delete_file(&video_path).await;
            if let Some(thumb) = thumbnail_path {
                let _ = s3.delete_file(&thumb).await;
            }
        }

        let result = sqlx::query("DELETE FROM project_session_videos WHERE id = $1")
            .bind(id)
            .execute(&*pool)
            .await?;

        Ok(result.rows_affected() > 0)
    }
}
