/**
 * Weekly phasing computation for estimate demand lines (4Dispo-style).
 *
 * Distributes total hours across ISO 8601 weeks. Supports:
 * - Even distribution
 * - Front-loaded (60/40 split)
 * - Back-loaded (40/60 split)
 * - Custom per-week overrides
 * - Aggregation to monthly spread
 * - Aggregation by chapter for 4Dispo view
 */

export interface WeekDefinition {
  weekNumber: number;
  year: number;
  startDate: string; // YYYY-MM-DD
  endDate: string; // YYYY-MM-DD
  label: string; // e.g. "W12 2026"
}

export interface WeeklyPhasingInput {
  totalHours: number;
  startDate: string; // YYYY-MM-DD
  endDate: string; // YYYY-MM-DD
  pattern?: "even" | "front_loaded" | "back_loaded" | "custom";
  customWeeklyHours?: Record<string, number>; // weekKey "2026-W12" -> hours
}

export interface WeeklyPhasingResult {
  weeks: WeekDefinition[];
  weeklyHours: Record<string, number>; // weekKey -> hours
  totalDistributedHours: number;
}

/**
 * Returns ISO 8601 week number and year for a given date.
 * Week 1 is the week containing January 4th; weeks start on Monday.
 */
function getISOWeekData(date: Date): { year: number; week: number } {
  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  // Set to nearest Thursday: current date + 4 - current day number (Monday=1, Sunday=7)
  const dayNum = d.getUTCDay() || 7;
  d.setUTCDate(d.getUTCDate() + 4 - dayNum);
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  const week = Math.ceil(((d.getTime() - yearStart.getTime()) / 86_400_000 + 1) / 7);
  return { year: d.getUTCFullYear(), week };
}

/**
 * Returns the Monday of the ISO week containing the given date.
 */
function getISOWeekMonday(date: Date): Date {
  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  const dayNum = d.getUTCDay() || 7; // Monday=1, Sunday=7
  d.setUTCDate(d.getUTCDate() - (dayNum - 1));
  return d;
}

/**
 * Converts a date (string or Date) to an ISO week key "YYYY-Www".
 */
export function weekKeyFromDate(date: Date | string): string {
  const d = typeof date === "string" ? new Date(date) : date;
  const { year, week } = getISOWeekData(d);
  return `${year}-W${String(week).padStart(2, "0")}`;
}

function formatDateISO(d: Date): string {
  const year = d.getUTCFullYear();
  const month = String(d.getUTCMonth() + 1).padStart(2, "0");
  const day = String(d.getUTCDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
}

/**
 * Generate ISO week definitions between two dates (inclusive).
 * Each week covers Monday-Sunday. Partial weeks at the boundaries are included.
 */
export function generateWeekRange(startDate: string, endDate: string): WeekDefinition[] {
  const start = new Date(startDate);
  const end = new Date(endDate);

  if (start > end) {
    return [];
  }

  const weeks: WeekDefinition[] = [];
  const seen = new Set<string>();

  // Start from the Monday of the week containing startDate
  let monday = getISOWeekMonday(start);

  while (monday.getTime() <= end.getTime()) {
    const { year, week } = getISOWeekData(monday);
    const key = `${year}-W${String(week).padStart(2, "0")}`;

    if (!seen.has(key)) {
      seen.add(key);
      const sunday = new Date(monday);
      sunday.setUTCDate(sunday.getUTCDate() + 6);

      weeks.push({
        weekNumber: week,
        year,
        startDate: formatDateISO(monday),
        endDate: formatDateISO(sunday),
        label: `W${String(week).padStart(2, "0")} ${year}`,
      });
    }

    // Move to next Monday
    monday = new Date(monday);
    monday.setUTCDate(monday.getUTCDate() + 7);
  }

  return weeks;
}

/**
 * Distribute total hours across weeks according to the specified pattern.
 */
export function distributeHoursToWeeks(input: WeeklyPhasingInput): WeeklyPhasingResult {
  const { totalHours, startDate, endDate, pattern = "even", customWeeklyHours } = input;
  const weeks = generateWeekRange(startDate, endDate);

  if (weeks.length === 0) {
    return { weeks: [], weeklyHours: {}, totalDistributedHours: 0 };
  }

  const weeklyHours: Record<string, number> = {};

  if (pattern === "custom" && customWeeklyHours) {
    // Use custom values, defaulting missing weeks to 0
    for (const week of weeks) {
      const key = `${week.year}-W${String(week.weekNumber).padStart(2, "0")}`;
      weeklyHours[key] = customWeeklyHours[key] ?? 0;
    }
  } else if (pattern === "even") {
    const perWeek = totalHours / weeks.length;
    for (const week of weeks) {
      const key = `${week.year}-W${String(week.weekNumber).padStart(2, "0")}`;
      weeklyHours[key] = Math.round(perWeek * 100) / 100;
    }
    // Fix rounding error on last week
    adjustRoundingError(weeklyHours, weeks, totalHours);
  } else if (pattern === "front_loaded") {
    distributeLoadedPattern(weeklyHours, weeks, totalHours, "front");
  } else if (pattern === "back_loaded") {
    distributeLoadedPattern(weeklyHours, weeks, totalHours, "back");
  }

  const totalDistributedHours =
    Math.round(Object.values(weeklyHours).reduce((sum, h) => sum + h, 0) * 100) / 100;

  return { weeks, weeklyHours, totalDistributedHours };
}

/**
 * Distribute hours with a linear ramp (front or back loaded).
 * Front: 60% first half, 40% second half with linear decrease
 * Back: 40% first half, 60% second half with linear increase
 */
function distributeLoadedPattern(
  weeklyHours: Record<string, number>,
  weeks: WeekDefinition[],
  totalHours: number,
  direction: "front" | "back",
): void {
  const n = weeks.length;

  if (n === 1) {
    const key = `${weeks[0]!.year}-W${String(weeks[0]!.weekNumber).padStart(2, "0")}`;
    weeklyHours[key] = totalHours;
    return;
  }

  // Create linear ramp weights
  // Front: weight decreases from high to low
  // Back: weight increases from low to high
  const weights: number[] = [];
  for (let i = 0; i < n; i++) {
    if (direction === "front") {
      weights.push(n - i); // n, n-1, ..., 1
    } else {
      weights.push(i + 1); // 1, 2, ..., n
    }
  }

  const totalWeight = weights.reduce((sum, w) => sum + w, 0);

  for (let i = 0; i < n; i++) {
    const week = weeks[i]!;
    const key = `${week.year}-W${String(week.weekNumber).padStart(2, "0")}`;
    weeklyHours[key] = Math.round((totalHours * (weights[i]! / totalWeight)) * 100) / 100;
  }

  adjustRoundingError(weeklyHours, weeks, totalHours);
}

function adjustRoundingError(
  weeklyHours: Record<string, number>,
  weeks: WeekDefinition[],
  totalHours: number,
): void {
  const distributed = Object.values(weeklyHours).reduce((sum, h) => sum + h, 0);
  const diff = Math.round((totalHours - distributed) * 100) / 100;
  if (diff !== 0 && weeks.length > 0) {
    const lastWeek = weeks[weeks.length - 1]!;
    const lastKey = `${lastWeek.year}-W${String(lastWeek.weekNumber).padStart(2, "0")}`;
    weeklyHours[lastKey] = Math.round(((weeklyHours[lastKey] ?? 0) + diff) * 100) / 100;
  }
}

/**
 * Convert weekly hours (keyed by "YYYY-Www") to monthly totals (keyed by "YYYY-MM").
 *
 * Each week's hours are attributed to the month containing the Thursday of that week
 * (which matches the ISO week-numbering year's month attribution).
 */
export function aggregateWeeklyToMonthly(
  weeklyHours: Record<string, number>,
): Record<string, number> {
  const monthly: Record<string, number> = {};

  for (const [weekKey, hours] of Object.entries(weeklyHours)) {
    const monthKey = weekKeyToMonthKey(weekKey);
    monthly[monthKey] = Math.round(((monthly[monthKey] ?? 0) + hours) * 100) / 100;
  }

  return monthly;
}

/**
 * Determine the month key for a week key by finding the Thursday of that week.
 */
function weekKeyToMonthKey(weekKey: string): string {
  const match = weekKey.match(/^(\d{4})-W(\d{2})$/);
  if (!match) {
    throw new Error(`Invalid week key: ${weekKey}`);
  }

  const year = parseInt(match[1]!, 10);
  const week = parseInt(match[2]!, 10);

  // Find January 4th of that year (always in ISO week 1)
  const jan4 = new Date(Date.UTC(year, 0, 4));
  const jan4DayOfWeek = jan4.getUTCDay() || 7; // Monday=1
  // Monday of week 1
  const week1Monday = new Date(jan4);
  week1Monday.setUTCDate(jan4.getUTCDate() - (jan4DayOfWeek - 1));

  // Monday of the target week
  const targetMonday = new Date(week1Monday);
  targetMonday.setUTCDate(week1Monday.getUTCDate() + (week - 1) * 7);

  // Thursday of the target week
  const thursday = new Date(targetMonday);
  thursday.setUTCDate(targetMonday.getUTCDate() + 3);

  const monthNum = String(thursday.getUTCMonth() + 1).padStart(2, "0");
  return `${thursday.getUTCFullYear()}-${monthNum}`;
}

/**
 * Aggregate weekly hours across multiple demand lines, grouped by chapter.
 * Returns a map of chapter -> weekKey -> total hours.
 * Lines with null/undefined chapter are grouped under "(Unassigned)".
 */
export function aggregateWeeklyByChapter(
  lines: Array<{ chapter?: string | null; weeklyHours: Record<string, number> }>,
): Record<string, Record<string, number>> {
  const result: Record<string, Record<string, number>> = {};

  for (const line of lines) {
    const chapter = line.chapter ?? "(Unassigned)";
    if (!result[chapter]) {
      result[chapter] = {};
    }
    const chapterTotals = result[chapter]!;

    for (const [weekKey, hours] of Object.entries(line.weeklyHours)) {
      chapterTotals[weekKey] = Math.round(((chapterTotals[weekKey] ?? 0) + hours) * 100) / 100;
    }
  }

  return result;
}
