import { z, type Issue, type ValidationResult } from '../shared/validation';
import { MoneyDecimal, roundedProduct, sumMoney, percentageMoney } from '../shared/decimal';
import { isDate, addDays, workingDays, calendarDays, nextWorkingDay } from '../shared/dates';

export const PLANNER_LIMITS = { tasks: 100, fileBytes: 256_000 } as const;
const decimal = (maximum: number) => z.string().regex(/^\d{1,9}(?:\.\d{1,4})?$/, 'Use a positive number with up to four decimal places.').refine(value => Number(value) <= maximum, `Use ${maximum.toLocaleString('en-CA')} or less.`);
const date = z.string().refine(isDate, 'Use a real date in YYYY-MM-DD format.');
const id = z.string().regex(/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/, 'Use a valid task ID.');

export const taskSchema = z.strictObject({
  id,
  phase: z.string().trim().min(1).max(80),
  title: z.string().trim().min(1).max(160),
  start: date,
  end: date,
  hours: decimal(100_000),
  rate: decimal(1_000_000),
  progressPercent: decimal(100),
  predecessors: z.array(id).max(PLANNER_LIMITS.tasks),
});

export const planSchema = z.strictObject({
  schemaVersion: z.literal(1),
  kind: z.literal('project-plan'),
  title: z.string().trim().min(1).max(160),
  description: z.string().max(4000),
  currency: z.enum(['CAD', 'USD', 'EUR', 'GBP', 'AUD']),
  referenceDate: date,
  contingencyPercent: decimal(100),
  tasks: z.array(taskSchema).min(1, 'Add at least one task.').max(PLANNER_LIMITS.tasks),
});
export const schema = planSchema;
export type Plan = z.infer<typeof planSchema>;
export type PlanTask = Plan['tasks'][number];

/** Validate the saved application model, never Frappe's mutable task objects. */
export function validatePlan(input: unknown): ValidationResult<Plan> {
  const warnings: Issue[] = [];
  const unsafe = inspectInput(input);
  if (unsafe) return { ok: false, errors: [{ path: '', message: unsafe }], warnings };
  const parsed = planSchema.safeParse(input);
  if (!parsed.success) return {
    ok: false,
    errors: parsed.error.issues.map(issue => ({ path: issue.path.join('.'), message: issue.message })),
    warnings,
  };
  const plan = parsed.data;
  const errors: Issue[] = [];
  const byId = new Map<string, PlanTask>();
  plan.tasks.forEach((task, index) => {
    if (byId.has(task.id)) errors.push({ path: `tasks.${index}.id`, message: 'Each task needs a unique ID.' });
    byId.set(task.id, task);
    if (task.end < task.start) errors.push({ path: `tasks.${index}.end`, message: 'The end date must be on or after the start date.' });
    if (new Set(task.predecessors).size !== task.predecessors.length) errors.push({ path: `tasks.${index}.predecessors`, message: 'A predecessor can only be listed once.' });
  });
  plan.tasks.forEach((task, index) => {
    task.predecessors.forEach(predecessor => {
      const preceding = byId.get(predecessor);
      if (predecessor === task.id) errors.push({ path: `tasks.${index}.predecessors`, message: 'A task cannot depend on itself.' });
      else if (!preceding) errors.push({ path: `tasks.${index}.predecessors`, message: 'A predecessor task is missing. Choose an existing task.' });
      else if (task.start < nextWorkingDay(preceding.end)) warnings.push({
        path: `tasks.${index}.start`,
        message: `“${task.title}” starts before the next working day after “${preceding.title}” finishes. Dates have not been moved.`,
      });
    });
  });
  const visited = new Set<string>();
  const visiting = new Set<string>();
  const visit = (taskId: string): boolean => {
    if (visiting.has(taskId)) return false;
    if (visited.has(taskId)) return true;
    visiting.add(taskId);
    for (const predecessor of byId.get(taskId)?.predecessors ?? []) if (!visit(predecessor)) return false;
    visiting.delete(taskId);
    visited.add(taskId);
    return true;
  };
  if (plan.tasks.some(task => !visit(task.id))) errors.push({ path: 'tasks', message: 'These dependencies form a loop. Remove a predecessor to break the cycle.' });
  return errors.length ? { ok: false, errors, warnings } : { ok: true, value: plan, warnings };
}

// Bounds apply before schema traversal, including callers outside the browser.
function inspectInput(input: unknown): string | null {
  const seen = new Set<object>();
  let nodes = 0;
  const inspect = (value: unknown, depth: number): string | null => {
    if (++nodes > 6000 || depth > 8) return 'This plan is too complex to import.';
    if (typeof value === 'string' && value.length > 8000) return 'A field in this plan is too long.';
    if (!value || typeof value !== 'object') return null;
    if (seen.has(value)) return 'This plan contains a circular object.';
    seen.add(value);
    if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return 'This plan contains an unsupported object.';
    for (const [key, child] of Object.entries(value)) {
      if (['__proto__', 'prototype', 'constructor'].includes(key)) return 'This plan contains an unsafe property.';
      const problem = inspect(child, depth + 1);
      if (problem) return problem;
    }
    seen.delete(value);
    return null;
  };
  return inspect(input, 0);
}

export interface PlanSummary {
  totalHours: string;
  baseCost: string;
  contingency: string;
  totalCost: string;
  taskCosts: Record<string, string>;
  start: string;
  end: string;
  calendarDays: number;
  workingDays: number;
}

export function calculatePlan(plan: Plan): PlanSummary {
  const checked = validatePlan(plan);
  if (!checked.ok) throw new Error('Correct the plan before calculating its budget.');
  const taskCosts: Record<string, string> = Object.create(null);
  let hours = new MoneyDecimal(0);
  for (const task of plan.tasks) {
    taskCosts[task.id] = roundedProduct(task.hours, task.rate);
    hours = hours.plus(task.hours);
  }
  const base = sumMoney(Object.values(taskCosts));
  const contingency = percentageMoney(base, plan.contingencyPercent);
  const start = plan.tasks.map(task => task.start).sort()[0];
  const end = plan.tasks.map(task => task.end).sort().at(-1)!;
  return {
    totalHours: hours.toString(), baseCost: base, contingency,
    totalCost: sumMoney([base, contingency]), taskCosts, start, end,
    calendarDays: calendarDays(start, end), workingDays: workingDays(start, end),
  };
}

/** Quote every cell; prefix formula-like text without changing numeric fields. */
function csvCell(value: string, numeric = false): string {
  const safe = !numeric && /^[\s\u0000-\u001f]*[=+@-]|^[\t\r\n]/.test(value) ? `'${value}` : value;
  return `"${safe.replaceAll('"', '""')}"`;
}

export function exportPlanCsv(plan: Plan): string {
  const summary = calculatePlan(plan);
  const rows = [[
    'Task ID', 'Phase', 'Task', 'Start (inclusive)', 'End (inclusive)', 'Hours', 'Hourly rate', 'Task cost', 'Currency', 'Progress (%)', 'Predecessor IDs',
  ].map(value => csvCell(value)).join(',')];
  for (const task of plan.tasks) rows.push([
    csvCell(task.id), csvCell(task.phase), csvCell(task.title), csvCell(task.start), csvCell(task.end),
    csvCell(task.hours, true), csvCell(task.rate, true), csvCell(summary.taskCosts[task.id], true), csvCell(plan.currency),
    csvCell(task.progressPercent, true), csvCell(task.predecessors.join('; ')),
  ].join(','));
  return `\uFEFF${rows.join('\r\n')}\r\n`;
}

export function newTaskId(): string {
  return `task-${globalThis.crypto.randomUUID()}`;
}

function example(title: string, description: string, hours: number[], rate: string): Plan {
  const phases = ['Discovery', 'Design', 'Development', 'Integration', 'Testing', 'Launch'];
  const titles = ['Agree on goals and scope', 'Design the main screens and experience', 'Build the main features', 'Connect the required systems', 'Test with the team and fix issues', 'Prepare the launch and handover'];
  let start = '2026-09-07';
  const tasks = phases.map((phase, index) => {
    const end = addDays(start, index === 2 ? 18 : index === 5 ? 2 : 4);
    const task: PlanTask = {
      id: `task-${index + 1}`, phase, title: titles[index], start, end,
      hours: String(hours[index]), rate, progressPercent: '0', predecessors: index ? [`task-${index}`] : [],
    };
    start = nextWorkingDay(end);
    return task;
  });
  return { schemaVersion: 1, kind: 'project-plan', title, description, currency: 'CAD', referenceDate: '2026-09-07', contingencyPercent: '15', tasks };
}

export const plannerExamples = {
  'customer-portal': example('Customer portal', 'An illustrative plan for a secure customer portal with account access, requests and updates. Replace the sample hours, rates and dates with your own.', [24, 40, 160, 40, 48, 16], '100'),
  'internal-application': example('Internal business application', 'An illustrative plan for an internal application that replaces spreadsheet handoffs. Sample rates are not Essential Designs prices.', [32, 48, 200, 64, 64, 24], '100'),
  'legacy-modernization': example('Legacy software modernization', 'An illustrative plan for improving an existing system, connecting data and moving users safely. Investigate migration requirements before setting your own estimate.', [48, 40, 240, 96, 80, 32], '100'),
} satisfies Record<string, Plan>;
