import { prisma } from '../prisma/client';
import { NotFoundError, BadRequestError } from '../errors/AppError';
import type { CreateClientTargetInput, UpdateClientTargetInput, CreateCorrectionInput } from '../types';
import { Prisma } from '@prisma/client';

// Returns the Monday of the week containing the given date
function getMondayOfWeek(date: Date): Date {
  const d = new Date(date);
  const day = d.getUTCDay(); // 0 = Sunday, 1 = Monday, ...
  const diff = day === 0 ? -6 : 1 - day;
  d.setUTCDate(d.getUTCDate() + diff);
  d.setUTCHours(0, 0, 0, 0);
  return d;
}

// Returns the first day of the month for a given date
function getFirstOfMonth(date: Date): Date {
  const d = new Date(date);
  d.setUTCDate(1);
  d.setUTCHours(0, 0, 0, 0);
  return d;
}

// Returns the last day of the month for a given date
function getLastOfMonth(date: Date): Date {
  const d = new Date(date);
  d.setUTCMonth(d.getUTCMonth() + 1, 0);
  d.setUTCHours(23, 59, 59, 999);
  return d;
}

// Returns all Mondays from startDate up to and including the current week's Monday
function getWeekMondays(startDate: Date): Date[] {
  const mondays: Date[] = [];
  const currentMonday = getMondayOfWeek(new Date());
  let cursor = new Date(startDate);
  cursor.setUTCHours(0, 0, 0, 0);
  while (cursor <= currentMonday) {
    mondays.push(new Date(cursor));
    cursor.setUTCDate(cursor.getUTCDate() + 7);
  }
  return mondays;
}

// Returns all month starts from startDate up to and including the current month
function getMonthStarts(startDate: Date): Date[] {
  const months: Date[] = [];
  const currentMonthStart = getFirstOfMonth(new Date());
  let cursor = getFirstOfMonth(new Date(startDate));
  while (cursor <= currentMonthStart) {
    months.push(new Date(cursor));
    cursor.setUTCMonth(cursor.getUTCMonth() + 1);
  }
  return months;
}

// Get all working days in a period that fall on the given weekdays
function getWorkingDaysInPeriod(
  periodStart: Date,
  periodEnd: Date,
  workDays: number[]
): Date[] {
  const workingDays: Date[] = [];
  const current = new Date(periodStart);
  current.setUTCHours(0, 0, 0, 0);
  
  while (current <= periodEnd) {
    const dayOfWeek = current.getUTCDay() === 0 ? 7 : current.getUTCDay(); // Convert to ISO (1=Mon, 7=Sun)
    if (workDays.includes(dayOfWeek)) {
      workingDays.push(new Date(current));
    }
    current.setUTCDate(current.getUTCDate() + 1);
  }
  
  return workingDays;
}

// Get working days in a period up to and including today
function getWorkingDaysUpToToday(
  periodStart: Date,
  periodEnd: Date,
  workDays: number[]
): Date[] {
  const today = new Date();
  today.setUTCHours(0, 0, 0, 0);
  const endDate = periodEnd < today ? periodEnd : today;
  return getWorkingDaysInPeriod(periodStart, endDate, workDays);
}

interface DayBalance {
  date: string; // YYYY-MM-DD
  trackedSeconds: number;
  targetSeconds: number;
  balanceSeconds: number;
}

export interface ClientTargetWithBalance {
  id: string;
  clientId: string;
  clientName: string;
  userId: string;
  periodType: 'weekly' | 'monthly';
  targetHours: number;
  workDays: number[];
  startDate: string;
  createdAt: string;
  updatedAt: string;
  corrections: Array<{
    id: string;
    date: string;
    hours: number;
    description: string | null;
    createdAt: string;
  }>;
  totalBalanceSeconds: number;
  currentPeriodTrackedSeconds: number;
  currentPeriodTargetSeconds: number;
  days: DayBalance[];
}

export class ClientTargetService {
  async findAll(userId: string): Promise<ClientTargetWithBalance[]> {
    const targets = await prisma.clientTarget.findMany({
      where: { userId, deletedAt: null, client: { deletedAt: null } },
      include: {
        client: { select: { id: true, name: true } },
        corrections: { where: { deletedAt: null }, orderBy: { date: 'asc' } },
      },
      orderBy: { client: { name: 'asc' } },
    });

    return Promise.all(targets.map(t => this.computeBalance(t)));
  }

  async findById(id: string, userId: string) {
    return prisma.clientTarget.findFirst({
      where: { id, userId, deletedAt: null, client: { deletedAt: null } },
      include: {
        client: { select: { id: true, name: true } },
        corrections: { where: { deletedAt: null }, orderBy: { date: 'asc' } },
      },
    });
  }

  async create(userId: string, data: CreateClientTargetInput): Promise<ClientTargetWithBalance> {
    // Validate startDate format
    const startDate = new Date(data.startDate + 'T00:00:00Z');
    if (isNaN(startDate.getTime())) {
      throw new BadRequestError('Invalid startDate format');
    }

    // Validate startDate based on periodType
    if (data.periodType === 'weekly') {
      const dayOfWeek = startDate.getUTCDay();
      if (dayOfWeek !== 1) {
        throw new BadRequestError('For weekly targets, startDate must be a Monday');
      }
    } else if (data.periodType === 'monthly') {
      if (startDate.getUTCDate() !== 1) {
        throw new BadRequestError('For monthly targets, startDate must be the 1st of a month');
      }
    }

    // Validate workDays
    if (!data.workDays || data.workDays.length === 0) {
      throw new BadRequestError('At least one working day must be selected');
    }
    if (new Set(data.workDays).size !== data.workDays.length) {
      throw new BadRequestError('workDays must not contain duplicates');
    }

    // Ensure the client belongs to this user and is not soft-deleted
    const client = await prisma.client.findFirst({ where: { id: data.clientId, userId, deletedAt: null } });
    if (!client) {
      throw new NotFoundError('Client not found');
    }

    // Check for existing target (unique per user+client)
    const existing = await prisma.clientTarget.findFirst({ where: { userId, clientId: data.clientId } });
    if (existing) {
      if (existing.deletedAt !== null) {
        // Reactivate the soft-deleted target with the new settings
        const reactivated = await prisma.clientTarget.update({
          where: { id: existing.id },
          data: {
            deletedAt: null,
            targetHours: data.targetHours,
            periodType: data.periodType,
            workDays: data.workDays,
            startDate,
          },
          include: {
            client: { select: { id: true, name: true } },
            corrections: { where: { deletedAt: null }, orderBy: { date: 'asc' } },
          },
        });
        return this.computeBalance(reactivated);
      }
      throw new BadRequestError('A target already exists for this client. Delete the existing one first or update it.');
    }

    const target = await prisma.clientTarget.create({
      data: {
        userId,
        clientId: data.clientId,
        targetHours: data.targetHours,
        periodType: data.periodType,
        workDays: data.workDays,
        startDate,
      },
      include: {
        client: { select: { id: true, name: true } },
        corrections: { where: { deletedAt: null }, orderBy: { date: 'asc' } },
      },
    });

    return this.computeBalance(target);
  }

  async update(id: string, userId: string, data: UpdateClientTargetInput): Promise<ClientTargetWithBalance> {
    const existing = await this.findById(id, userId);
    if (!existing) throw new NotFoundError('Client target not found');

    const updateData: {
      targetHours?: number;
      periodType?: string;
      workDays?: number[];
      startDate?: Date;
    } = {};

    if (data.targetHours !== undefined) {
      updateData.targetHours = data.targetHours;
    }

    if (data.periodType !== undefined) {
      updateData.periodType = data.periodType;
    }

    if (data.workDays !== undefined) {
      if (data.workDays.length === 0) {
        throw new BadRequestError('At least one working day must be selected');
      }
      if (new Set(data.workDays).size !== data.workDays.length) {
        throw new BadRequestError('workDays must not contain duplicates');
      }
      updateData.workDays = data.workDays;
    }

    if (data.startDate !== undefined) {
      const startDate = new Date(data.startDate + 'T00:00:00Z');
      if (isNaN(startDate.getTime())) {
        throw new BadRequestError('Invalid startDate format');
      }

      const periodType = data.periodType ?? existing.periodType;
      if (periodType === 'weekly') {
        if (startDate.getUTCDay() !== 1) {
          throw new BadRequestError('For weekly targets, startDate must be a Monday');
        }
      } else if (periodType === 'monthly') {
        if (startDate.getUTCDate() !== 1) {
          throw new BadRequestError('For monthly targets, startDate must be the 1st of a month');
        }
      }
      updateData.startDate = startDate;
    }

    const updated = await prisma.clientTarget.update({
      where: { id },
      data: updateData,
      include: {
        client: { select: { id: true, name: true } },
        corrections: { where: { deletedAt: null }, orderBy: { date: 'asc' } },
      },
    });

    return this.computeBalance(updated);
  }

  async delete(id: string, userId: string): Promise<void> {
    const existing = await this.findById(id, userId);
    if (!existing) throw new NotFoundError('Client target not found');
    await prisma.clientTarget.update({
      where: { id },
      data: { deletedAt: new Date() },
    });
  }

  async addCorrection(targetId: string, userId: string, data: CreateCorrectionInput) {
    const target = await this.findById(targetId, userId);
    if (!target) throw new NotFoundError('Client target not found');

    const correctionDate = new Date(data.date + 'T00:00:00Z');
    const startDate = new Date(target.startDate);
    startDate.setUTCHours(0, 0, 0, 0);

    if (correctionDate < startDate) {
      throw new BadRequestError('Correction date cannot be before the target start date');
    }

    return prisma.balanceCorrection.create({
      data: {
        clientTargetId: targetId,
        date: correctionDate,
        hours: data.hours,
        description: data.description,
      },
    });
  }

  async deleteCorrection(targetId: string, correctionId: string, userId: string): Promise<void> {
    const target = await this.findById(targetId, userId);
    if (!target) throw new NotFoundError('Client target not found');

    const correction = await prisma.balanceCorrection.findFirst({
      where: { id: correctionId, clientTargetId: targetId, deletedAt: null },
    });
    if (!correction) throw new NotFoundError('Correction not found');

    await prisma.balanceCorrection.update({
      where: { id: correctionId },
      data: { deletedAt: new Date() },
    });
  }

  private async computeBalance(target: {
    id: string;
    clientId: string;
    userId: string;
    targetHours: number;
    periodType: string;
    workDays: number[];
    startDate: Date;
    createdAt: Date;
    updatedAt: Date;
    client: { id: string; name: string };
    corrections: Array<{ id: string; date: Date; hours: number; description: string | null; createdAt: Date }>;
  }): Promise<ClientTargetWithBalance> {
    const startDateObj = new Date(target.startDate);
    startDateObj.setUTCHours(0, 0, 0, 0);

    // Get all periods (weeks or months) from startDate to now
    const periods: Array<{ start: Date; end: Date }> = [];
    if (target.periodType === 'weekly') {
      const mondays = getWeekMondays(startDateObj);
      for (const monday of mondays) {
        const sunday = new Date(monday);
        sunday.setUTCDate(sunday.getUTCDate() + 6);
        sunday.setUTCHours(23, 59, 59, 999);
        periods.push({ start: monday, end: sunday });
      }
    } else {
      const months = getMonthStarts(startDateObj);
      for (const monthStart of months) {
        const monthEnd = getLastOfMonth(monthStart);
        periods.push({ start: monthStart, end: monthEnd });
      }
    }

    if (periods.length === 0) {
      return this.emptyBalance(target);
    }

    // Fetch all tracked time for this user on this client's projects
    const periodStart = periods[0].start;
    const periodEnd = periods[periods.length - 1].end;

    type TrackedRow = { date: string; tracked_seconds: bigint };

    const rows = await prisma.$queryRaw<TrackedRow[]>(Prisma.sql`
      SELECT
        DATE(te.start_time AT TIME ZONE 'UTC') AS date,
        COALESCE(SUM(EXTRACT(EPOCH FROM (te.end_time - te.start_time)) - (te.break_minutes * 60)), 0)::bigint AS tracked_seconds
      FROM time_entries te
      JOIN projects p ON p.id = te.project_id
      WHERE te.user_id = ${target.userId}
        AND p.client_id = ${target.clientId}
        AND DATE(te.start_time AT TIME ZONE 'UTC') >= ${periodStart}::date
        AND DATE(te.start_time AT TIME ZONE 'UTC') <= ${periodEnd}::date
        AND te.deleted_at IS NULL
        AND p.deleted_at IS NULL
      GROUP BY DATE(te.start_time AT TIME ZONE 'UTC')
      ORDER BY date ASC
    `);

    // Index tracked seconds by date (ISO string)
    const trackedByDate = new Map<string, number>();
    for (const row of rows) {
      trackedByDate.set(row.date, Number(row.tracked_seconds));
    }

    // Index corrections by period
    const correctionsByPeriod = new Map<number, number>(); // period index -> total hours
    for (const c of target.corrections) {
      const correctionDate = new Date(c.date);
      correctionDate.setUTCHours(0, 0, 0, 0);

      for (let i = 0; i < periods.length; i++) {
        if (correctionDate >= periods[i].start && correctionDate <= periods[i].end) {
          correctionsByPeriod.set(i, (correctionsByPeriod.get(i) ?? 0) + c.hours);
          break;
        }
      }
    }

    // Compute daily balances
    const days: DayBalance[] = [];
    let totalBalanceSeconds = 0;
    let currentPeriodTrackedSeconds = 0;
    let currentPeriodTargetSeconds = 0;

    const today = new Date();
    today.setUTCHours(0, 0, 0, 0);

    for (let periodIdx = 0; periodIdx < periods.length; periodIdx++) {
      const period = periods[periodIdx];
      
      // Get working days for this period up to today
      const workingDaysInPeriod = getWorkingDaysUpToToday(
        period.start,
        period.end,
        target.workDays
      );

      if (workingDaysInPeriod.length === 0) {
        continue; // No working days in this period up to today
      }

      // Calculate effective target for this period (after corrections)
      const correctionHours = correctionsByPeriod.get(periodIdx) ?? 0;
      const effectiveTargetSeconds = (target.targetHours * 3600) - (correctionHours * 3600);
      const targetSecondsPerDay = effectiveTargetSeconds / workingDaysInPeriod.length;

      // Is this the current period?
      const isCurrentPeriod = periodIdx === periods.length - 1;
      if (isCurrentPeriod) {
        // For full month/week target, count all working days in the period (for display)
        const allWorkingDaysInPeriod = getWorkingDaysInPeriod(period.start, period.end, target.workDays);
        currentPeriodTargetSeconds = (target.targetHours * 3600) - (correctionHours * 3600);
      }

      // Process each working day
      for (const workingDay of workingDaysInPeriod) {
        const dateStr = workingDay.toISOString().split('T')[0];
        const trackedSeconds = trackedByDate.get(dateStr) ?? 0;
        const balanceSeconds = trackedSeconds - targetSecondsPerDay;

        days.push({
          date: dateStr,
          trackedSeconds,
          targetSeconds: targetSecondsPerDay,
          balanceSeconds,
        });

        totalBalanceSeconds += balanceSeconds;

        if (isCurrentPeriod) {
          currentPeriodTrackedSeconds += trackedSeconds;
        }
      }
    }

    // If no days were added but we have periods, use defaults
    if (days.length === 0) {
      const lastPeriod = periods[periods.length - 1];
      const correctionHours = correctionsByPeriod.get(periods.length - 1) ?? 0;
      currentPeriodTargetSeconds = (target.targetHours * 3600) - (correctionHours * 3600);
    }

    return {
      id: target.id,
      clientId: target.clientId,
      clientName: target.client.name,
      userId: target.userId,
      periodType: target.periodType as 'weekly' | 'monthly',
      targetHours: target.targetHours,
      workDays: target.workDays,
      startDate: target.startDate.toISOString().split('T')[0],
      createdAt: target.createdAt.toISOString(),
      updatedAt: target.updatedAt.toISOString(),
      corrections: target.corrections.map(c => ({
        id: c.id,
        date: c.date.toISOString().split('T')[0],
        hours: c.hours,
        description: c.description,
        createdAt: c.createdAt.toISOString(),
      })),
      totalBalanceSeconds,
      currentPeriodTrackedSeconds,
      currentPeriodTargetSeconds,
      days,
    };
  }

  private emptyBalance(target: {
    id: string;
    clientId: string;
    userId: string;
    targetHours: number;
    periodType: string;
    workDays: number[];
    startDate: Date;
    createdAt: Date;
    updatedAt: Date;
    client: { id: string; name: string };
    corrections: Array<{ id: string; date: Date; hours: number; description: string | null; createdAt: Date }>;
  }): ClientTargetWithBalance {
    return {
      id: target.id,
      clientId: target.clientId,
      clientName: target.client.name,
      userId: target.userId,
      periodType: target.periodType as 'weekly' | 'monthly',
      targetHours: target.targetHours,
      workDays: target.workDays,
      startDate: target.startDate.toISOString().split('T')[0],
      createdAt: target.createdAt.toISOString(),
      updatedAt: target.updatedAt.toISOString(),
      corrections: [],
      totalBalanceSeconds: 0,
      currentPeriodTrackedSeconds: 0,
      currentPeriodTargetSeconds: target.targetHours * 3600,
      days: [],
    };
  }
}
