import { z } from "zod";
import { planWorkBlocksIntentSchema } from "@/lib/intent";

export const busySlotSchema = z.object({
  start: z.string().min(1),
  end: z.string().min(1)
});

export const workingHoursSchema = z.object({
  timezone: z.string().default("Europe/Paris"),
  startHour: z.number().int().min(0).max(23).default(9),
  endHour: z.number().int().min(1).max(23).default(18),
  excludedWeekdays: z.array(z.number().int().min(0).max(6)).default([0, 6])
});

export const workPlanBlockSchema = z.object({
  title: z.string().min(1),
  start: z.string().min(1),
  end: z.string().min(1),
  durationMinutes: z.number().int().positive(),
  notes: z.string().nullable().default(null)
});

export const workPlanSchema = z.object({
  planId: z.string().min(1),
  source: z.literal("work_planner_v1"),
  provider: z.enum(["outlook", "google"]),
  taskTitle: z.string().min(1),
  timezone: z.string().default("Europe/Paris"),
  totalEffortMinutes: z.number().int().positive(),
  plannedMinutes: z.number().int().nonnegative(),
  remainingMinutes: z.number().int().nonnegative(),
  deadline: z.string().min(1),
  blocks: z.array(workPlanBlockSchema),
  diagnostics: z
    .object({
      cadence_type: z.enum(["none", "daily", "weekly", "custom"]),
      weeks_detected: z.number().int().positive(),
      target_minutes_per_week: z.number().int().positive().nullable(),
      allocation_reason: z.string().min(1)
    })
    .default({
      cadence_type: "none",
      weeks_detected: 1,
      target_minutes_per_week: null,
      allocation_reason: "default_spread"
    }),
  warnings: z.array(z.string()).default([]),
  plannerLog: z.array(z.string()).default([])
});

export type BusySlot = z.infer<typeof busySlotSchema>;
export type WorkingHours = z.infer<typeof workingHoursSchema>;
export type WorkPlan = z.infer<typeof workPlanSchema>;

type TimeWindow = {
  start: Date;
  end: Date;
};

function clamp(value: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, value));
}

function isoDayKey(date: Date): string {
  return date.toISOString().slice(0, 10);
}

function parseDate(input: string): Date | null {
  const parsed = new Date(input);
  return Number.isNaN(parsed.valueOf()) ? null : parsed;
}

function overlaps(a: TimeWindow, b: TimeWindow): boolean {
  return a.start < b.end && b.start < a.end;
}

function subtractBusy(base: TimeWindow, busySlots: TimeWindow[]): TimeWindow[] {
  let free: TimeWindow[] = [base];

  for (const busy of busySlots) {
    const next: TimeWindow[] = [];

    for (const chunk of free) {
      if (!overlaps(chunk, busy)) {
        next.push(chunk);
        continue;
      }

      if (busy.start > chunk.start) {
        next.push({ start: chunk.start, end: new Date(busy.start) });
      }

      if (busy.end < chunk.end) {
        next.push({ start: new Date(busy.end), end: chunk.end });
      }
    }

    free = next;
  }

  return free.filter((slot) => slot.end > slot.start);
}

function buildDailyWindows(
  now: Date,
  deadline: Date,
  workingHours: WorkingHours,
  plannerLog: string[]
): TimeWindow[] {
  const windows: TimeWindow[] = [];
  const cursor = new Date(now);
  cursor.setHours(0, 0, 0, 0);

  while (cursor <= deadline) {
    const weekday = cursor.getDay();
    if (!workingHours.excludedWeekdays.includes(weekday)) {
      const dayStart = new Date(cursor);
      dayStart.setHours(workingHours.startHour, 0, 0, 0);

      const dayEnd = new Date(cursor);
      dayEnd.setHours(workingHours.endHour, 0, 0, 0);

      const start = dayStart < now ? new Date(now) : dayStart;
      const end = dayEnd > deadline ? new Date(deadline) : dayEnd;

      if (end > start) {
        windows.push({ start, end });
      }
    }

    cursor.setDate(cursor.getDate() + 1);
  }

  plannerLog.push(`daily_windows=${windows.length}`);
  return windows;
}

function splitWindowIntoBlocks(
  window: TimeWindow,
  blockMinutes: number,
  breakMinutes: number,
  title: string
) {
  const blocks: Array<z.infer<typeof workPlanBlockSchema>> = [];

  let pointer = new Date(window.start);

  while (pointer.getTime() + blockMinutes * 60 * 1000 <= window.end.getTime()) {
    const end = new Date(pointer.getTime() + blockMinutes * 60 * 1000);

    blocks.push({
      title: `Travail: ${title}`,
      start: pointer.toISOString(),
      end: end.toISOString(),
      durationMinutes: blockMinutes,
      notes: null
    });

    pointer = new Date(end.getTime() + breakMinutes * 60 * 1000);
  }

  return blocks;
}

function allocateSpread(
  candidates: Array<z.infer<typeof workPlanBlockSchema>>,
  requiredBlocks: number
): Array<z.infer<typeof workPlanBlockSchema>> {
  const grouped = new Map<string, Array<z.infer<typeof workPlanBlockSchema>>>();

  for (const slot of candidates) {
    const key = isoDayKey(new Date(slot.start));
    const current = grouped.get(key) ?? [];
    current.push(slot);
    grouped.set(key, current);
  }

  const dayKeys = Array.from(grouped.keys()).sort();
  const selected: Array<z.infer<typeof workPlanBlockSchema>> = [];

  while (selected.length < requiredBlocks) {
    let addedInPass = 0;

    for (const key of dayKeys) {
      const slots = grouped.get(key) ?? [];
      const next = slots.shift();
      if (!next) continue;

      selected.push(next);
      addedInPass += 1;

      if (selected.length >= requiredBlocks) {
        break;
      }
    }

    if (addedInPass === 0) {
      break;
    }
  }

  return selected.sort((a, b) => +new Date(a.start) - +new Date(b.start));
}

function isoWeekKey(date: Date): string {
  const utc = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
  const dayNum = utc.getUTCDay() || 7;
  utc.setUTCDate(utc.getUTCDate() + 4 - dayNum);
  const yearStart = new Date(Date.UTC(utc.getUTCFullYear(), 0, 1));
  const weekNo = Math.ceil((((utc.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
  return `${utc.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`;
}

function dayStamp(date: Date): number {
  return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
}

function allocateWeeklyCadence(input: {
  candidates: Array<z.infer<typeof workPlanBlockSchema>>;
  requiredBlocks: number;
  blockMinutes: number;
  targetMinutesPerWeek: number;
}): Array<z.infer<typeof workPlanBlockSchema>> {
  const grouped = new Map<string, Array<z.infer<typeof workPlanBlockSchema>>>();
  const counts = new Map<string, number>();
  const cursors = new Map<string, number>();

  for (const slot of input.candidates) {
    const key = isoWeekKey(new Date(slot.start));
    const current = grouped.get(key) ?? [];
    current.push(slot);
    grouped.set(key, current);
  }

  const weekKeys = Array.from(grouped.keys()).sort();
  weekKeys.forEach((key) => {
    counts.set(key, 0);
    cursors.set(key, 0);
  });

  const selected: Array<z.infer<typeof workPlanBlockSchema>> = [];
  const perWeekLimit = Math.max(1, Math.round(input.targetMinutesPerWeek / input.blockMinutes));

  while (selected.length < input.requiredBlocks) {
    let addedInPass = 0;

    for (const key of weekKeys) {
      const slots = grouped.get(key) ?? [];
      const cursor = cursors.get(key) ?? 0;
      const count = counts.get(key) ?? 0;

      if (cursor >= slots.length || count >= perWeekLimit) {
        continue;
      }

      selected.push(slots[cursor]);
      cursors.set(key, cursor + 1);
      counts.set(key, count + 1);
      addedInPass += 1;

      if (selected.length >= input.requiredBlocks) {
        break;
      }
    }

    if (addedInPass === 0) {
      break;
    }
  }

  // If strict weekly target cannot satisfy requested workload, fill remaining slots progressively.
  while (selected.length < input.requiredBlocks) {
    let addedInPass = 0;

    for (const key of weekKeys) {
      const slots = grouped.get(key) ?? [];
      const cursor = cursors.get(key) ?? 0;
      if (cursor >= slots.length) {
        continue;
      }

      selected.push(slots[cursor]);
      cursors.set(key, cursor + 1);
      addedInPass += 1;

      if (selected.length >= input.requiredBlocks) {
        break;
      }
    }

    if (addedInPass === 0) {
      break;
    }
  }

  return selected.sort((a, b) => +new Date(a.start) - +new Date(b.start));
}

function allocateByGapDays(input: {
  candidates: Array<z.infer<typeof workPlanBlockSchema>>;
  requiredBlocks: number;
  minGapDays: number;
}): Array<z.infer<typeof workPlanBlockSchema>> {
  const selected: Array<z.infer<typeof workPlanBlockSchema>> = [];
  const used = new Set<string>();
  const gap = Math.max(0, input.minGapDays);

  for (const slot of input.candidates) {
    if (selected.length >= input.requiredBlocks) break;

    const currentStamp = dayStamp(new Date(slot.start));
    const lastStamp = selected.length
      ? dayStamp(new Date(selected[selected.length - 1].start))
      : null;

    if (lastStamp !== null) {
      const dayDiff = Math.floor((currentStamp - lastStamp) / 86400000);
      if (dayDiff < gap) {
        continue;
      }
    }

    selected.push(slot);
    used.add(`${slot.start}|${slot.end}`);
  }

  if (selected.length >= input.requiredBlocks) {
    return selected;
  }

  for (const slot of input.candidates) {
    if (selected.length >= input.requiredBlocks) break;
    const key = `${slot.start}|${slot.end}`;
    if (used.has(key)) continue;

    selected.push(slot);
    used.add(key);
  }

  return selected;
}

export function buildWorkPlan(input: {
  provider: "outlook" | "google";
  intent: z.infer<typeof planWorkBlocksIntentSchema>;
  busySlots: BusySlot[];
  workingHours?: WorkingHours;
  now?: Date;
}): WorkPlan {
  const now = input.now ? new Date(input.now) : new Date();
  const deadlineRaw = parseDate(input.intent.deadline);
  const plannerLog: string[] = [];
  const warnings: string[] = [];

  const safeDeadline =
    deadlineRaw && deadlineRaw > now
      ? deadlineRaw
      : new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);

  const normalizedWorkingHours = workingHoursSchema.parse({
    ...input.workingHours,
    timezone: input.intent.timezone,
    startHour: input.intent.constraints.work_day_start_hour,
    endHour: input.intent.constraints.work_day_end_hour,
    excludedWeekdays: input.intent.constraints.excluded_weekdays
  });

  const totalEffortMinutes = clamp(input.intent.total_effort_minutes, 30, 24 * 60);
  const preferredBlockMinutes = clamp(input.intent.preferred_block_minutes, 25, 180);
  const minBreakMinutes = clamp(input.intent.constraints.min_break_minutes, 0, 90);

  if (!deadlineRaw || deadlineRaw <= now) {
    warnings.push("Deadline absente/invalide: utilisation d'une deadline par defaut a J+7.");
  }

  const requestedBlockCount = Math.ceil(totalEffortMinutes / preferredBlockMinutes);
  if (requestedBlockCount > 24) {
    warnings.push("Nombre de blocs limite a 24 pour eviter une creation massive.");
  }

  const maxBlocks = Math.min(requestedBlockCount, 24);
  const weeksDetected = Math.max(
    1,
    Math.ceil((safeDeadline.getTime() - now.getTime()) / (7 * 24 * 60 * 60 * 1000))
  );

  const normalizedBusy = input.busySlots
    .map((slot) => {
      const start = parseDate(slot.start);
      const end = parseDate(slot.end);
      if (!start || !end || end <= start) return null;
      return { start, end };
    })
    .filter((slot): slot is TimeWindow => Boolean(slot))
    .sort((a, b) => +a.start - +b.start);

  plannerLog.push(`busy_slots=${normalizedBusy.length}`);

  const dailyWindows = buildDailyWindows(now, safeDeadline, normalizedWorkingHours, plannerLog);

  const candidates: Array<z.infer<typeof workPlanBlockSchema>> = [];

  for (const window of dailyWindows) {
    const intersectingBusy = normalizedBusy.filter((busy) => overlaps(window, busy));
    const freeWindows = subtractBusy(window, intersectingBusy);

    for (const free of freeWindows) {
      const blocks = splitWindowIntoBlocks(
        free,
        preferredBlockMinutes,
        minBreakMinutes,
        input.intent.task_title
      );
      candidates.push(...blocks);
    }
  }

  plannerLog.push(`candidate_blocks=${candidates.length}`);

  const preferredDays = input.intent.preferred_days;
  const filteredCandidates =
    preferredDays.length > 0
      ? candidates.filter((slot) => preferredDays.includes(new Date(slot.start).getUTCDay()))
      : candidates;

  const effectiveCandidates =
    preferredDays.length > 0 && filteredCandidates.length > 0 ? filteredCandidates : candidates;

  if (preferredDays.length > 0 && filteredCandidates.length === 0) {
    warnings.push("Aucun creneau sur jours preferes: retour a l'allocation standard.");
  }

  let allocationReason = "default_spread";
  let selected: Array<z.infer<typeof workPlanBlockSchema>>;

  if (
    input.intent.cadence_type === "weekly" &&
    input.intent.cadence_per_week_target_minutes
  ) {
    selected = allocateWeeklyCadence({
      candidates: effectiveCandidates,
      requiredBlocks: maxBlocks,
      blockMinutes: preferredBlockMinutes,
      targetMinutesPerWeek: input.intent.cadence_per_week_target_minutes
    });
    allocationReason = "weekly_cadence_distribution";
  } else if (input.intent.cadence_type === "daily") {
    selected = allocateByGapDays({
      candidates: effectiveCandidates,
      requiredBlocks: maxBlocks,
      minGapDays: 1
    });
    allocationReason = "daily_cadence_distribution";
  } else if (input.intent.cadence_type === "custom" && input.intent.cadence_every_n_days) {
    selected = allocateByGapDays({
      candidates: effectiveCandidates,
      requiredBlocks: maxBlocks,
      minGapDays: input.intent.cadence_every_n_days
    });
    allocationReason = "custom_cadence_distribution";
  } else {
    selected =
      input.intent.constraints.strategy === "compact"
        ? effectiveCandidates.slice(0, maxBlocks)
        : allocateSpread(effectiveCandidates, maxBlocks);
    allocationReason =
      input.intent.constraints.strategy === "compact"
        ? "compact_earliest_slots"
        : "spread_day_distribution";
  }

  const plannedMinutes = selected.reduce((acc, block) => acc + block.durationMinutes, 0);
  const remainingMinutes = Math.max(0, totalEffortMinutes - plannedMinutes);

  if (selected.length === 0) {
    warnings.push("Aucun creneau libre trouve avant la deadline.");
  } else if (remainingMinutes > 0) {
    warnings.push(
      `Plan partiel: ${remainingMinutes} min non planifiees avant la deadline.`
    );
  }

  const planId = `plan_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;

  return workPlanSchema.parse({
    planId,
    source: "work_planner_v1",
    provider: input.provider,
    taskTitle: input.intent.task_title,
    timezone: normalizedWorkingHours.timezone,
    totalEffortMinutes,
    plannedMinutes,
    remainingMinutes,
    deadline: safeDeadline.toISOString(),
    blocks: selected,
    diagnostics: {
      cadence_type: input.intent.cadence_type,
      weeks_detected: weeksDetected,
      target_minutes_per_week: input.intent.cadence_per_week_target_minutes,
      allocation_reason: allocationReason
    },
    warnings,
    plannerLog
  });
}
