import { db } from '../config/database.js';
import { timeEntries, projects, todos, companies, users, timesheets } from '../db/schema.js';
import { eq, and, gte, lte, desc } from 'drizzle-orm';
import { NotFoundError, BadRequestError, ForbiddenError } from '../utils/errors.js';
import ExcelJS from 'exceljs';
import fs from 'fs/promises';
import path from 'path';

// Helpers to normalize optional payload fields
const normalizeOptionalId = (value) => {
  if (value === undefined) return undefined;
  if (value === null || value === '') return null;
  return value;
};

const normalizeOptionalText = (value) => {
  if (value === undefined) return undefined;
  if (value === null) return null;
  if (typeof value !== 'string') return value;
  const trimmed = value.trim();
  return trimmed.length ? trimmed : null;
};

const formatDate = (value) => {
  const date = new Date(value);
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
};

const formatTime = (value) => {
  const date = new Date(value);
  const hours = String(date.getHours()).padStart(2, '0');
  const minutes = String(date.getMinutes()).padStart(2, '0');
  return `${hours}:${minutes}`;
};

const formatDuration = (minutes) => {
  if (!Number.isFinite(minutes)) return '';
  const hours = Math.floor(minutes / 60);
  const mins = Math.abs(minutes % 60);
  return `${hours}:${String(mins).padStart(2, '0')}`;
};

/**
 * Start a new time entry
 */
export const startTimeEntry = async (userId, data) => {
  const projectId = normalizeOptionalId(data.projectId);
  const todoId = normalizeOptionalId(data.todoId);
  const companyId = normalizeOptionalId(data.companyId);
  const description = normalizeOptionalText(data.description);

  // Check if user already has a running time entry
  const [existingRunning] = await db
    .select()
    .from(timeEntries)
    .where(and(eq(timeEntries.userId, userId), eq(timeEntries.isRunning, true)))
    .limit(1);

  // Automatically stop existing running timer
  if (existingRunning) {
    const endTime = new Date();
    const durationInMinutes = Math.round((endTime - new Date(existingRunning.startTime)) / 60000);

    await db
      .update(timeEntries)
      .set({
        endTime,
        duration: durationInMinutes,
        isRunning: false,
        updatedAt: new Date(),
      })
      .where(eq(timeEntries.id, existingRunning.id));
  }

  // 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 todo exists if provided
  if (todoId) {
    const [todo] = await db
      .select()
      .from(todos)
      .where(eq(todos.id, todoId))
      .limit(1);

    if (!todo) {
      throw new NotFoundError('Todo 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á');
    }
  }

  const [newEntry] = await db
    .insert(timeEntries)
    .values({
      userId,
      projectId: projectId ?? null,
      todoId: todoId ?? null,
      companyId: companyId ?? null,
      description: description ?? null,
      startTime: new Date(),
      endTime: null,
      duration: null,
      isRunning: true,
      isEdited: false,
    })
    .returning();

  return newEntry;
};

/**
 * Stop a running time entry
 */
export const stopTimeEntry = async (entryId, userId, data = {}) => {
  const projectId = normalizeOptionalId(data.projectId);
  const todoId = normalizeOptionalId(data.todoId);
  const companyId = normalizeOptionalId(data.companyId);
  const description = normalizeOptionalText(data.description);

  const entry = await getTimeEntryById(entryId);

  // Verify ownership
  if (entry.userId !== userId) {
    throw new BadRequestError('Nemáte oprávnenie zastaviť tento časovač');
  }

  if (!entry.isRunning) {
    throw new BadRequestError('Tento časovač už nie je spustený');
  }

  const endTime = new Date();
  const durationInMinutes = Math.round((endTime - new Date(entry.startTime)) / 60000);

  // Verify related entities if provided (skip validation for null/undefined)
  if (projectId) {
    const [project] = await db
      .select()
      .from(projects)
      .where(eq(projects.id, projectId))
      .limit(1);

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

  if (todoId) {
    const [todo] = await db
      .select()
      .from(todos)
      .where(eq(todos.id, todoId))
      .limit(1);

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

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

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

  const [updated] = await db
    .update(timeEntries)
    .set({
      endTime,
      duration: durationInMinutes,
      isRunning: false,
      projectId: projectId !== undefined ? projectId : entry.projectId,
      todoId: todoId !== undefined ? todoId : entry.todoId,
      companyId: companyId !== undefined ? companyId : entry.companyId,
      description: description !== undefined ? description : entry.description,
      updatedAt: new Date(),
    })
    .where(eq(timeEntries.id, entryId))
    .returning();

  return updated;
};

/**
 * Get running time entry for a user
 */
export const getRunningTimeEntry = async (userId) => {
  const [running] = await db
    .select()
    .from(timeEntries)
    .where(and(eq(timeEntries.userId, userId), eq(timeEntries.isRunning, true)))
    .limit(1);

  return running || null;
};

/**
 * Get all running time entries (for dashboard)
 */
export const getAllRunningTimeEntries = async () => {
  const result = await db
    .select({
      id: timeEntries.id,
      userId: timeEntries.userId,
      startTime: timeEntries.startTime,
      firstName: users.firstName,
      lastName: users.lastName,
    })
    .from(timeEntries)
    .innerJoin(users, eq(timeEntries.userId, users.id))
    .where(eq(timeEntries.isRunning, true));

  return result;
};

/**
 * Get time entry by ID
 */
export const getTimeEntryById = async (entryId) => {
  const [entry] = await db
    .select()
    .from(timeEntries)
    .where(eq(timeEntries.id, entryId))
    .limit(1);

  if (!entry) {
    throw new NotFoundError('Záznam nenájdený');
  }

  return entry;
};

/**
 * Get all time entries for a user with optional filters
 */
export const getAllTimeEntries = async (userId, filters = {}) => {
  const { projectId, todoId, companyId, startDate, endDate } = filters;

  let query = db.select().from(timeEntries).where(eq(timeEntries.userId, userId));

  const conditions = [eq(timeEntries.userId, userId)];

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

  if (todoId) {
    conditions.push(eq(timeEntries.todoId, todoId));
  }

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

  if (startDate) {
    conditions.push(gte(timeEntries.startTime, new Date(startDate)));
  }

  if (endDate) {
    conditions.push(lte(timeEntries.startTime, new Date(endDate)));
  }

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

  const result = await query.orderBy(desc(timeEntries.startTime));
  return result;
};

/**
 * Get time entries for a specific month
 */
export const getMonthlyTimeEntries = async (userId, year, month) => {
  const startDate = new Date(year, month - 1, 1);
  const endDate = new Date(year, month, 0, 23, 59, 59, 999);

  return await db
    .select()
    .from(timeEntries)
    .where(
      and(
        eq(timeEntries.userId, userId),
        gte(timeEntries.startTime, startDate),
        lte(timeEntries.startTime, endDate)
      )
    )
    .orderBy(desc(timeEntries.startTime));
};

/**
 * Generate an XLSX timesheet for a given month and user.
 */
export const generateMonthlyTimesheet = async (userId, year, month) => {
  const startDate = new Date(year, month - 1, 1);
  const endDate = new Date(year, month, 0, 23, 59, 59, 999);

  const [user] = await db
    .select({
      username: users.username,
      firstName: users.firstName,
      lastName: users.lastName,
    })
    .from(users)
    .where(eq(users.id, userId))
    .limit(1);

  if (!user) {
    throw new NotFoundError('Používateľ nenájdený');
  }

  const entries = await db
    .select({
      id: timeEntries.id,
      startTime: timeEntries.startTime,
      endTime: timeEntries.endTime,
      duration: timeEntries.duration,
      description: timeEntries.description,
      projectName: projects.name,
      todoTitle: todos.title,
      companyName: companies.name,
    })
    .from(timeEntries)
    .leftJoin(projects, eq(timeEntries.projectId, projects.id))
    .leftJoin(todos, eq(timeEntries.todoId, todos.id))
    .leftJoin(companies, eq(timeEntries.companyId, companies.id))
    .where(
      and(
        eq(timeEntries.userId, userId),
        gte(timeEntries.startTime, startDate),
        lte(timeEntries.startTime, endDate)
      )
    )
    .orderBy(timeEntries.startTime);

  const completedEntries = entries.filter((entry) => entry.endTime && entry.duration !== null);
  if (completedEntries.length === 0) {
    throw new NotFoundError('Žiadne dokončené záznamy pre daný mesiac');
  }

  let totalMinutes = 0;
  const dailyTotals = {};

  completedEntries.forEach((entry) => {
    const minutes = entry.duration || 0;
    totalMinutes += minutes;
    const day = formatDate(entry.startTime);
    dailyTotals[day] = (dailyTotals[day] || 0) + minutes;
  });

  const workbook = new ExcelJS.Workbook();
  workbook.creator = 'crm-server';
  const worksheet = workbook.addWorksheet('Timesheet', {
    views: [{ state: 'frozen', ySplit: 6 }],
  });

  const fullName = [user.firstName, user.lastName].filter(Boolean).join(' ') || user.username;
  const periodLabel = `${year}-${String(month).padStart(2, '0')}`;

  worksheet.getCell('A1').value = 'Timesheet';
  worksheet.getCell('A1').font = { name: 'Calibri', size: 16, bold: true };
  worksheet.mergeCells('A1:D1');

  worksheet.getCell('A2').value = `Name: ${fullName}`;
  worksheet.getCell('A3').value = `Period: ${periodLabel}`;
  worksheet.getCell('A4').value = `Generated: ${new Date().toLocaleString()}`;

  worksheet.columns = [
    { key: 'date', width: 12 },
    { key: 'project', width: 28 },
    { key: 'todo', width: 28 },
    { key: 'company', width: 24 },
    { key: 'description', width: 40 },
    { key: 'start', width: 12 },
    { key: 'end', width: 12 },
    { key: 'duration', width: 16 },
  ];

  const headerRowNumber = 6;
  const headerRow = worksheet.getRow(headerRowNumber);
  headerRow.values = [
    'Date',
    'Project',
    'Todo',
    'Company',
    'Description',
    'Start',
    'End',
    'Duration (h:mm)',
  ];
  headerRow.font = { name: 'Calibri', size: 11, bold: true };
  headerRow.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
  headerRow.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE8EEF5' } };
  headerRow.height = 20;

  completedEntries.forEach((entry) => {
    worksheet.addRow({
      date: formatDate(entry.startTime),
      project: entry.projectName || '',
      todo: entry.todoTitle || '',
      company: entry.companyName || '',
      description: entry.description || '',
      start: formatTime(entry.startTime),
      end: formatTime(entry.endTime),
      duration: formatDuration(entry.duration),
    });
  });

  // Style body rows
  worksheet.eachRow((row, rowNumber) => {
    if (rowNumber >= headerRowNumber) {
      row.alignment = { vertical: 'middle', wrapText: true };
    }
  });

  let summaryStart = worksheet.lastRow.number + 2;
  worksheet.getCell(`A${summaryStart}`).value = 'Daily totals';
  worksheet.getCell(`A${summaryStart}`).font = { name: 'Calibri', size: 12, bold: true };
  worksheet.mergeCells(`A${summaryStart}:B${summaryStart}`);

  const dailyHeaderRowNumber = summaryStart + 1;
  const dailyHeaderRow = worksheet.getRow(dailyHeaderRowNumber);
  dailyHeaderRow.values = ['Date', 'Total (h:mm)'];
  dailyHeaderRow.font = { name: 'Calibri', size: 11, bold: true };
  dailyHeaderRow.alignment = { vertical: 'middle', horizontal: 'center' };
  dailyHeaderRow.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE8EEF5' } };

  Object.keys(dailyTotals)
    .sort()
    .forEach((day) => {
      worksheet.addRow([day, formatDuration(dailyTotals[day])]);
    });

  const overallRow = worksheet.addRow(['Overall total', formatDuration(totalMinutes)]);
  overallRow.font = { name: 'Calibri', size: 11, bold: true };

  const uploadsDir = path.join(
    process.cwd(),
    'uploads',
    'timesheets',
    userId.toString(),
    year.toString(),
    month.toString()
  );
  await fs.mkdir(uploadsDir, { recursive: true });

  const filename = `timesheet-${periodLabel}-${Date.now()}.xlsx`;
  const filePath = path.join(uploadsDir, filename);
  let savedFilePath = null;

  try {
    await workbook.xlsx.writeFile(filePath);
    savedFilePath = filePath;
    const { size: fileSize } = await fs.stat(filePath);

    const [newTimesheet] = await db
      .insert(timesheets)
      .values({
        userId,
        fileName: filename,
        filePath,
        fileType: 'xlsx',
        fileSize,
        year,
        month,
        isGenerated: true,
      })
      .returning();

    return {
      timesheet: {
        id: newTimesheet.id,
        fileName: newTimesheet.fileName,
        fileType: newTimesheet.fileType,
        fileSize: newTimesheet.fileSize,
        year: newTimesheet.year,
        month: newTimesheet.month,
        isGenerated: newTimesheet.isGenerated,
        uploadedAt: newTimesheet.uploadedAt,
      },
      totals: {
        totalMinutes,
        totalFormatted: formatDuration(totalMinutes),
        daily: dailyTotals,
      },
    };
  } catch (error) {
    if (savedFilePath) {
      await fs.unlink(savedFilePath).catch(() => {});
    }
    throw error;
  }
};

/**
 * Update time entry
 */
export const updateTimeEntry = async (entryId, actor, data) => {
  const { userId, role } = actor;
  const entry = await getTimeEntryById(entryId);

  // Verify ownership (admin can edit anyone)
  if (entry.userId !== userId && role !== 'admin') {
    throw new ForbiddenError('Nemáte oprávnenie upraviť tento záznam');
  }

  if (entry.isRunning) {
    throw new BadRequestError('Nemôžete upraviť bežiaci časovač. Najprv ho zastavte.');
  }

  const { startTime, endTime } = data;
  const projectId = normalizeOptionalId(data.projectId);
  const todoId = normalizeOptionalId(data.todoId);
  const companyId = normalizeOptionalId(data.companyId);
  const description = normalizeOptionalText(data.description);

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

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

  if (todoId) {
    const [todo] = await db
      .select()
      .from(todos)
      .where(eq(todos.id, todoId))
      .limit(1);

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

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

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

  // Calculate new duration if times are changed
  let newDuration = entry.duration;
  const newStartTime = startTime ? new Date(startTime) : new Date(entry.startTime);
  const newEndTime = endTime ? new Date(endTime) : (entry.endTime ? new Date(entry.endTime) : null);

  if (newEndTime) {
    if (newEndTime <= newStartTime) {
      throw new BadRequestError('Čas ukončenia musí byť po čase začiatku');
    }
    newDuration = Math.round((newEndTime - newStartTime) / 60000);
  }

  const [updated] = await db
    .update(timeEntries)
    .set({
      startTime: newStartTime,
      endTime: newEndTime,
      duration: newDuration,
      projectId: projectId !== undefined ? projectId : entry.projectId,
      todoId: todoId !== undefined ? todoId : entry.todoId,
      companyId: companyId !== undefined ? companyId : entry.companyId,
      description: description !== undefined ? description : entry.description,
      isEdited: true,
      updatedAt: new Date(),
    })
    .where(eq(timeEntries.id, entryId))
    .returning();

  return updated;
};

/**
 * Delete time entry
 */
export const deleteTimeEntry = async (entryId, actor) => {
  const { userId, role } = actor;
  const entry = await getTimeEntryById(entryId);

  // Verify ownership (admin can delete anyone)
  if (entry.userId !== userId && role !== 'admin') {
    throw new ForbiddenError('Nemáte oprávnenie odstrániť tento záznam');
  }

  if (entry.isRunning) {
    throw new BadRequestError('Nemôžete odstrániť bežiaci časovač. Najprv ho zastavte.');
  }

  await db.delete(timeEntries).where(eq(timeEntries.id, entryId));

  return { success: true, message: 'Záznam bol odstránený' };
};

/**
 * Get time entry with related data (project, todo, company)
 */
export const getTimeEntryWithRelations = async (entryId) => {
  const entry = await getTimeEntryById(entryId);

  // Get project if exists
  let project = null;
  if (entry.projectId) {
    [project] = await db
      .select()
      .from(projects)
      .where(eq(projects.id, entry.projectId))
      .limit(1);
  }

  // Get todo if exists
  let todo = null;
  if (entry.todoId) {
    [todo] = await db
      .select()
      .from(todos)
      .where(eq(todos.id, entry.todoId))
      .limit(1);
  }

  // Get company if exists
  let company = null;
  if (entry.companyId) {
    [company] = await db
      .select()
      .from(companies)
      .where(eq(companies.id, entry.companyId))
      .limit(1);
  }

  return {
    ...entry,
    project,
    todo,
    company,
  };
};

/**
 * Get monthly statistics for a user
 */
export const getMonthlyStats = async (userId, year, month) => {
  const entries = await getMonthlyTimeEntries(userId, year, month);

  // Total time in minutes
  const totalMinutes = entries.reduce((sum, entry) => {
    return sum + (entry.duration || 0);
  }, 0);

  // Count of days worked
  const uniqueDays = new Set(
    entries
      .filter((e) => !e.isRunning)
      .map((e) => new Date(e.startTime).toDateString())
  ).size;

  // Time by project
  const byProject = {};
  entries.forEach((entry) => {
    if (entry.projectId && entry.duration) {
      if (!byProject[entry.projectId]) {
        byProject[entry.projectId] = 0;
      }
      byProject[entry.projectId] += entry.duration;
    }
  });

  // Time by company
  const byCompany = {};
  entries.forEach((entry) => {
    if (entry.companyId && entry.duration) {
      if (!byCompany[entry.companyId]) {
        byCompany[entry.companyId] = 0;
      }
      byCompany[entry.companyId] += entry.duration;
    }
  });

  return {
    totalMinutes,
    totalHours: Math.floor(totalMinutes / 60),
    remainingMinutes: totalMinutes % 60,
    daysWorked: uniqueDays,
    averagePerDay: uniqueDays > 0 ? Math.round(totalMinutes / uniqueDays) : 0,
    entriesCount: entries.length,
    byProject,
    byCompany,
  };
};
