import { db } from '../config/database.js';
import { todos, todoUsers, notes, projects, companies, users } from '../db/schema.js';
import { eq, desc, ilike, or, and, inArray, lt, ne, sql } from 'drizzle-orm';
import { NotFoundError } from '../utils/errors.js';
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
import { notifyNewTodoAssignment, notifyUpdatedTodoAssignment } from './todo-notification.service.js';
import {
  logTodoCreated, logTodoDeleted, logTodoCompleted, logTodoUpdated, logTodoUncompleted,
} from './audit.service.js';

/**
 * Get all todos
 * Optionally filter by search, project, company, assigned user, or status
 * For members: returns only todos they are assigned to
 */
export const getAllTodos = async (filters = {}, userId = null, userRole = null) => {
  const { searchTerm, projectId, companyId, assignedTo, status, priority } = filters;

  // Pre membera filtruj len todos kde je priradeny (admin a team_leader vidia vsetko)
  let accessibleTodoIds = null;
  if (userRole && userRole !== 'admin' && userRole !== 'team_leader' && userId) {
    accessibleTodoIds = await getAccessibleResourceIds('todo', userId);
    // Ak member nema pristup k ziadnym todos, vrat prazdne pole
    if (accessibleTodoIds.length === 0) {
      return [];
    }
  }

  // Build query conditions
  const conditions = [];

  // Member access filter - only todos they are assigned to
  if (accessibleTodoIds !== null) {
    conditions.push(inArray(todos.id, accessibleTodoIds));
  }

  // If filtering by assignedTo, we need additional filter
  if (assignedTo) {
    const todoIdsWithUser = await db
      .select({ todoId: todoUsers.todoId })
      .from(todoUsers)
      .where(eq(todoUsers.userId, assignedTo));

    const assignedTodoIds = todoIdsWithUser.map((row) => row.todoId);

    if (assignedTodoIds.length === 0) {
      return [];
    }

    conditions.push(inArray(todos.id, assignedTodoIds));
  }

  if (searchTerm) {
    conditions.push(
      or(
        ilike(todos.title, `%${searchTerm}%`),
        ilike(todos.description, `%${searchTerm}%`)
      )
    );
  }

  if (projectId) {
    conditions.push(eq(todos.projectId, projectId));
  }

  if (companyId) {
    conditions.push(eq(todos.companyId, companyId));
  }

  if (status) {
    conditions.push(eq(todos.status, status));
  }

  if (priority) {
    conditions.push(eq(todos.priority, priority));
  }

  let query = db.select().from(todos);

  if (conditions.length > 0) {
    query = query.where(and(...conditions));
  }

  const result = await query.orderBy(desc(todos.createdAt));

  // Fetch assigned users for all todos
  if (result.length > 0) {
    const resultTodoIds = result.map(todo => todo.id);
    const assignedUsersData = await db
      .select({
        todoId: todoUsers.todoId,
        odUserId: users.id,
        username: users.username,
        firstName: users.firstName,
        lastName: users.lastName,
      })
      .from(todoUsers)
      .innerJoin(users, eq(todoUsers.userId, users.id))
      .where(inArray(todoUsers.todoId, resultTodoIds));

    // Group assigned users by todoId
    const usersByTodoId = {};
    for (const row of assignedUsersData) {
      if (!usersByTodoId[row.todoId]) {
        usersByTodoId[row.todoId] = [];
      }
      usersByTodoId[row.todoId].push({
        id: row.odUserId,
        username: row.username,
        firstName: row.firstName,
        lastName: row.lastName,
      });
    }

    // Attach assigned users to each todo
    return result.map(todo => ({
      ...todo,
      assignedUsers: usersByTodoId[todo.id] || [],
    }));
  }

  return result;
};

/**
 * Get todo by ID
 */
export const getTodoById = async (todoId) => {
  const [todo] = await db
    .select()
    .from(todos)
    .where(eq(todos.id, todoId))
    .limit(1);

  if (!todo) {
    throw new NotFoundError('Todo nenájdené');
  }

  return todo;
};

/**
 * Create new todo
 * @param {string} userId - ID of user creating the todo
 * @param {object} data - Todo data including assignedUserIds array
 */
export const createTodo = async (userId, data, auditContext = null) => {
  const { title, description, projectId, companyId, assignedUserIds, status, priority, dueDate } = data;

  // Verify project exists if provided
  if (projectId) {
    const [project] = await db
      .select()
      .from(projects)
      .where(eq(projects.id, projectId))
      .limit(1);

    if (!project) {
      throw new NotFoundError('Projekt nenájdený');
    }
  }

  // Verify company exists if provided
  if (companyId) {
    const [company] = await db
      .select()
      .from(companies)
      .where(eq(companies.id, companyId))
      .limit(1);

    if (!company) {
      throw new NotFoundError('Firma nenájdená');
    }
  }

  // Verify assigned users exist if provided
  if (assignedUserIds && Array.isArray(assignedUserIds) && assignedUserIds.length > 0) {
    const existingUsers = await db
      .select({ id: users.id })
      .from(users)
      .where(inArray(users.id, assignedUserIds));

    if (existingUsers.length !== assignedUserIds.length) {
      throw new NotFoundError('Niektorí používatelia neboli nájdení');
    }
  }

  // Create the todo
  const [newTodo] = await db
    .insert(todos)
    .values({
      title,
      description: description || null,
      projectId: projectId || null,
      companyId: companyId || null,
      status: status || 'pending',
      priority: priority || 'medium',
      dueDate: dueDate ? new Date(dueDate) : null,
      createdBy: userId,
    })
    .returning();

  // Assign users to the todo
  if (assignedUserIds && Array.isArray(assignedUserIds) && assignedUserIds.length > 0) {
    const todoUserInserts = assignedUserIds.map((assignedUserId) => ({
      todoId: newTodo.id,
      userId: assignedUserId,
      assignedBy: userId,
    }));

    await db.insert(todoUsers).values(todoUserInserts);
  }

  // Send push notifications to assigned users (excluding creator)
  await notifyNewTodoAssignment(assignedUserIds, title, newTodo.id, userId);

  if (auditContext) {
    await logTodoCreated(auditContext.userId, newTodo.id, newTodo.title, auditContext.ipAddress, auditContext.userAgent);
  }

  return newTodo;
};

/**
 * Update todo
 * @param {string} todoId - ID of todo to update
 * @param {object} data - Updated data including assignedUserIds array
 * @param {string} updatedByUserId - ID of user making the update (for notifications)
 */
export const updateTodo = async (todoId, data, updatedByUserId = null, auditContext = null) => {
  const todo = await getTodoById(todoId);

  const { title, description, projectId, companyId, assignedUserIds, status, priority, dueDate } = data;

  // Verify project exists if being changed
  if (projectId !== undefined && projectId !== null && projectId !== todo.projectId) {
    const [project] = await db
      .select()
      .from(projects)
      .where(eq(projects.id, projectId))
      .limit(1);

    if (!project) {
      throw new NotFoundError('Projekt nenájdený');
    }
  }

  // Verify company exists if being changed
  if (companyId !== undefined && companyId !== null && companyId !== todo.companyId) {
    const [company] = await db
      .select()
      .from(companies)
      .where(eq(companies.id, companyId))
      .limit(1);

    if (!company) {
      throw new NotFoundError('Firma nenájdená');
    }
  }

  // Verify assigned users exist if being changed
  if (assignedUserIds !== undefined && Array.isArray(assignedUserIds) && assignedUserIds.length > 0) {
    const existingUsers = await db
      .select({ id: users.id })
      .from(users)
      .where(inArray(users.id, assignedUserIds));

    if (existingUsers.length !== assignedUserIds.length) {
      throw new NotFoundError('Niektorí používatelia neboli nájdení');
    }
  }

  // Set completedAt when status is changed to 'completed'
  let completedAt = todo.completedAt;
  if (status === 'completed' && todo.status !== 'completed') {
    completedAt = new Date();
  } else if (status && status !== 'completed') {
    completedAt = null;
  }

  const [updated] = await db
    .update(todos)
    .set({
      title: title !== undefined ? title : todo.title,
      description: description !== undefined ? description : todo.description,
      projectId: projectId !== undefined ? projectId : todo.projectId,
      companyId: companyId !== undefined ? companyId : todo.companyId,
      status: status !== undefined ? status : todo.status,
      priority: priority !== undefined ? priority : todo.priority,
      dueDate: dueDate !== undefined ? (dueDate ? new Date(dueDate) : null) : todo.dueDate,
      completedAt,
      updatedAt: new Date(),
    })
    .where(eq(todos.id, todoId))
    .returning();

  // Update assigned users if provided
  if (assignedUserIds !== undefined) {
    // Get existing assigned users before deleting
    const existingAssignments = await db
      .select({ userId: todoUsers.userId })
      .from(todoUsers)
      .where(eq(todoUsers.todoId, todoId));
    const existingUserIds = existingAssignments.map(a => a.userId);

    // Delete existing assignments
    await db.delete(todoUsers).where(eq(todoUsers.todoId, todoId));

    // Create new assignments
    if (Array.isArray(assignedUserIds) && assignedUserIds.length > 0) {
      const todoUserInserts = assignedUserIds.map((userId) => ({
        todoId: todoId,
        userId: userId,
        assignedBy: updatedByUserId,
      }));

      await db.insert(todoUsers).values(todoUserInserts);

      // Send push notifications to newly assigned users
      const newlyAssignedUserIds = assignedUserIds.filter(
        id => !existingUserIds.includes(id)
      );
      await notifyUpdatedTodoAssignment(newlyAssignedUserIds, updated.title, todoId, updatedByUserId);
    }
  }

  if (auditContext) {
    // Detect status changes for specific audit events
    if (status === 'completed' && todo.status !== 'completed') {
      await logTodoCompleted(auditContext.userId, todoId, todo.title, auditContext.ipAddress, auditContext.userAgent);
    } else if (status && status !== 'completed' && todo.status === 'completed') {
      await logTodoUncompleted(auditContext.userId, todoId, todo.title, auditContext.ipAddress, auditContext.userAgent);
    } else {
      await logTodoUpdated(auditContext.userId, todoId, { title: todo.title }, { title: updated.title }, auditContext.ipAddress, auditContext.userAgent);
    }
  }

  return updated;
};

/**
 * Delete todo
 */
export const deleteTodo = async (todoId, auditContext = null) => {
  const todo = await getTodoById(todoId); // Check if exists

  await db.delete(todos).where(eq(todos.id, todoId));

  if (auditContext) {
    await logTodoDeleted(auditContext.userId, todoId, todo.title, auditContext.ipAddress, auditContext.userAgent);
  }

  return { success: true, message: 'Todo bolo odstránené' };
};

/**
 * Get todo with related data (notes, project, company, assigned users)
 * Optimized to use joins and Promise.all to avoid N+1 query problem
 */
export const getTodoWithRelations = async (todoId) => {
  // Fetch todo with project and company in single query using joins
  const [todoResult] = await db
    .select({
      todo: todos,
      project: projects,
      company: companies,
    })
    .from(todos)
    .leftJoin(projects, eq(todos.projectId, projects.id))
    .leftJoin(companies, eq(todos.companyId, companies.id))
    .where(eq(todos.id, todoId))
    .limit(1);

  if (!todoResult) {
    throw new NotFoundError('Todo nenajdene');
  }

  // Batch fetch for assigned users and notes in parallel
  const [assignedUsers, todoNotes] = await Promise.all([
    db
      .select({
        id: users.id,
        username: users.username,
        firstName: users.firstName,
        lastName: users.lastName,
        assignedAt: todoUsers.assignedAt,
      })
      .from(todoUsers)
      .innerJoin(users, eq(todoUsers.userId, users.id))
      .where(eq(todoUsers.todoId, todoId)),

    db
      .select()
      .from(notes)
      .where(eq(notes.todoId, todoId))
      .orderBy(desc(notes.createdAt)),
  ]);

  return {
    ...todoResult.todo,
    project: todoResult.project,
    company: todoResult.company,
    assignedUsers,
    notes: todoNotes,
  };
};

/**
 * Get todos by project ID
 */
export const getTodosByProjectId = async (projectId) => {
  return await db
    .select()
    .from(todos)
    .where(eq(todos.projectId, projectId))
    .orderBy(desc(todos.createdAt));
};

/**
 * Get todos by company ID
 */
export const getTodosByCompanyId = async (companyId) => {
  return await db
    .select()
    .from(todos)
    .where(eq(todos.companyId, companyId))
    .orderBy(desc(todos.createdAt));
};

/**
 * Get todos assigned to a user
 */
export const getTodosByUserId = async (userId) => {
  const todoIdsWithUser = await db
    .select({ todoId: todoUsers.todoId })
    .from(todoUsers)
    .where(eq(todoUsers.userId, userId));

  const todoIds = todoIdsWithUser.map((row) => row.todoId);

  if (todoIds.length === 0) {
    return [];
  }

  return await db
    .select()
    .from(todos)
    .where(inArray(todos.id, todoIds))
    .orderBy(desc(todos.createdAt));
};

/**
 * Get count of overdue todos for a user
 * Overdue = dueDate < now AND status !== 'completed'
 * For members: only counts todos they are assigned to
 */
export const getOverdueCount = async (userId, userRole) => {
  const now = new Date();

  // Get accessible todo IDs for non-admin/non-team_leader users
  let accessibleTodoIds = null;
  if (userRole && userRole !== 'admin' && userRole !== 'team_leader') {
    accessibleTodoIds = await getAccessibleResourceIds('todo', userId);
    if (accessibleTodoIds.length === 0) {
      return 0;
    }
  }

  const conditions = [
    lt(todos.dueDate, now),
    ne(todos.status, 'completed'),
  ];

  if (accessibleTodoIds !== null) {
    conditions.push(inArray(todos.id, accessibleTodoIds));
  }

  const result = await db
    .select({ count: sql`count(*)::int` })
    .from(todos)
    .where(and(...conditions));

  return result[0]?.count || 0;
};

/**
 * Get count of todos created by user that were completed but not yet notified
 * Returns todos where createdBy = userId AND status = 'completed' AND completedNotifiedAt IS NULL
 */
export const getCompletedByMeCount = async (userId) => {
  const result = await db
    .select({ count: sql`count(*)::int` })
    .from(todos)
    .where(
      and(
        eq(todos.createdBy, userId),
        eq(todos.status, 'completed'),
        sql`${todos.completedNotifiedAt} IS NULL`
      )
    );

  return result[0]?.count || 0;
};

/**
 * Mark all completed todos created by user as notified
 * Called when user opens the Todos page
 */
export const markCompletedAsNotified = async (userId) => {
  await db
    .update(todos)
    .set({ completedNotifiedAt: new Date() })
    .where(
      and(
        eq(todos.createdBy, userId),
        eq(todos.status, 'completed'),
        sql`${todos.completedNotifiedAt} IS NULL`
      )
    );

  return { success: true };
};

/**
 * Get combined todo counts for sidebar badges
 */
export const getTodoCounts = async (userId, userRole) => {
  const [overdueCount, completedByMeCount] = await Promise.all([
    getOverdueCount(userId, userRole),
    getCompletedByMeCount(userId),
  ]);

  return {
    overdueCount,
    completedByMeCount,
  };
};
