import { z, type ValidationResult } from '../shared/validation';
import { MoneyDecimal as Decimal, roundedProduct, sumMoney, percentageMoney } from '../shared/decimal';
import { isDate } from '../shared/dates';
import { inspectLogoDataUrl } from './logo';

export const DOCUMENT_VERSION = 1;
export const DOCUMENT_LIMITS = { items: 200, text: 8000, logoBytes: 2_000_000, jsonBytes: 3_000_000 } as const;
export const documentTitles = { quotation: 'Quotation', 'work-order': 'Work order', 'service-report': 'Service report' } as const;
export type DocumentTemplateId = keyof typeof documentTitles;
const text = (max: number = DOCUMENT_LIMITS.text) => z.string().max(max).refine(v => !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/u.test(v), 'Remove hidden control characters from this text.');
const required = (max = 240) => text(max).refine(v => v.trim().length > 0, 'Please enter a value.');
const decimal = (max: number, places = 6) => z.string().regex(new RegExp(`^(?:0|[1-9]\\d{0,10})(?:\\.\\d{1,${places}})?$`), 'Enter a positive number or zero, using a decimal point and no commas.').refine(v => { try { return new Decimal(v).lte(max); } catch { return false; } }, `Enter a number no greater than ${max.toLocaleString('en-CA')}.`);
const date = z.string().refine(isDate, 'Enter a real date in YYYY-MM-DD format.');
const optionalDate = z.union([z.literal(''), date]);
const item = { id: z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/), description: required(1200), quantity: decimal(1_000_000) };
const common = {
  schemaVersion: z.literal(1), kind: z.literal('document'), templateId: z.enum(['quotation', 'work-order', 'service-report']),
  paper: z.enum(['A4', 'Letter']), accent: z.enum(['#6B46FF', '#173B35', '#26364B', '#171717']),
  business: z.strictObject({ name: required(240), contact: text(1200) }),
  client: z.strictObject({ name: required(240), contact: text(1200) }),
  number: required(80), date, validUntil: optionalDate,
  location: text(800), person: text(240), description: text(), notes: text(), terms: text(),
  findings: text(), recommendations: text(), nextSteps: text(),
  logo: z.string().max(2_700_000).refine(v => { try { inspectLogoDataUrl(v); return true; } catch { return false; } }, 'Use a valid local PNG or JPEG logo, no larger than 2 MB or 4096 pixels per side.').optional(),
};
export const documentSchema = z.discriminatedUnion('pricing', [
  z.strictObject({ ...common, pricing: z.literal(true), currency: z.enum(['CAD', 'USD']), discount: decimal(1e14, 2), taxes: z.array(z.strictObject({ name: required(40), rate: decimal(100) })).max(2), items: z.array(z.strictObject({ ...item, unitPrice: decimal(100_000_000) })).max(DOCUMENT_LIMITS.items) }),
  z.strictObject({ ...common, pricing: z.literal(false), items: z.array(z.strictObject(item)).max(DOCUMENT_LIMITS.items) }),
]).superRefine((data, ctx) => {
  if (data.templateId === 'quotation' && !data.pricing) ctx.addIssue({ code: 'custom', path: ['pricing'], message: 'Quotations include pricing. Choose a work order or service report to hide prices.' });
  if (data.validUntil && data.validUntil < data.date) ctx.addIssue({ code: 'custom', path: ['validUntil'], message: 'The valid-until date must be on or after the issue date.' });
  if (new Set(data.items.map(i => i.id)).size !== data.items.length) ctx.addIssue({ code: 'custom', path: ['items'], message: 'Each line item needs a different ID.' });
  if (data.pricing && data.items.every(i => /^\d+(\.\d+)?$/.test(i.quantity) && /^\d+(\.\d+)?$/.test(i.unitPrice)) && /^\d+(\.\d+)?$/.test(data.discount)) {
    const subtotal = data.items.reduce((sum, line) => sum.plus(new Decimal(line.quantity).times(line.unitPrice).toDecimalPlaces(2, Decimal.ROUND_HALF_UP)), new Decimal(0));
    if (new Decimal(data.discount).gt(subtotal)) ctx.addIssue({ code: 'custom', path: ['discount'], message: 'The discount cannot be greater than the subtotal.' });
  }
});
export type BusinessDocument = z.infer<typeof documentSchema>;
export type DocumentItem = BusinessDocument['items'][number];

function inspectKeys(value: unknown, depth = 0): void {
  if (depth > 8) throw new Error('The document contains too many nested objects.');
  if (!value || typeof value !== 'object') return;
  if (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value))) throw new Error('Only plain data objects are supported.');
  if (Array.isArray(value) && value.length > DOCUMENT_LIMITS.items) throw new Error('Documents support up to 200 line items.');
  for (const key of Object.keys(value)) {
    if (['__proto__', 'prototype', 'constructor'].includes(key)) throw new Error('The document contains an unsupported object key.');
    inspectKeys((value as Record<string, unknown>)[key], depth + 1);
  }
}
export function validateDocument(input: unknown): ValidationResult<BusinessDocument> {
  try { inspectKeys(input); if (new TextEncoder().encode(JSON.stringify(input)).byteLength > DOCUMENT_LIMITS.jsonBytes) throw new Error('This document is too large. Keep the saved JSON below 3 MB.'); } catch (error) { return { ok: false, errors: [{ path: '', message: error instanceof Error ? error.message : 'Invalid document.' }], warnings: [] }; }
  const parsed = documentSchema.safeParse(input);
  if (!parsed.success) return { ok: false, errors: parsed.error.issues.map(i => ({ path: i.path.join('.'), message: i.message })), warnings: [] };
  const warnings = parsed.data.items.length === 0 ? [{ path: 'items', message: 'This document has no line items. Add items if you need them.' }] : [];
  return { ok: true, value: parsed.data, warnings };
}
export function calculateDocument(data: BusinessDocument) {
  if (!data.pricing) return { pricing: false as const, items: data.items.map(i => ({ id: i.id, description: i.description, quantity: i.quantity })) };
  const lines = data.items.map(i => ({ id: i.id, total: roundedProduct(i.quantity, i.unitPrice) }));
  const subtotal = new Decimal(sumMoney(lines.map(line => line.total)));
  const taxable = subtotal.minus(data.discount);
  const taxes = data.taxes.map(tax => ({ name: tax.name, rate: tax.rate, amount: percentageMoney(taxable.toFixed(2), tax.rate) }));
  const total = taxes.reduce((sum, tax) => sum.plus(tax.amount), taxable);
  return { pricing: true as const, currency: data.currency, lines, subtotal: subtotal.toFixed(2), discount: new Decimal(data.discount).toFixed(2), taxable: taxable.toFixed(2), taxes, total: total.toFixed(2) };
}
const sampleCommon = {
  schemaVersion: 1 as const, kind: 'document' as const, paper: 'A4' as const, accent: '#173B35' as const,
  business: { name: 'Northline Services', contact: '123 Example Avenue, Vancouver, BC\nhello@example.com · 604-555-0100' },
  client: { name: 'Cedar & Co.', contact: '456 Sample Street, Vancouver, BC\nTaylor Martin · client@example.com' },
  number: 'Q-2026-001', date: '2026-09-06', validUntil: '2026-10-06', location: '456 Sample Street, Vancouver, BC', person: 'Alex Morgan',
  description: 'Office maintenance and equipment setup.', notes: 'Thank you for the opportunity to support your team.', terms: 'Valid for 30 days. Please confirm the scope and schedule before work begins.', findings: '', recommendations: '', nextSteps: '',
};
export const documentExamples: Record<DocumentTemplateId, BusinessDocument> = {
  quotation: { ...sampleCommon, templateId: 'quotation', pricing: true, currency: 'CAD', discount: '0.00', taxes: [{ name: 'Tax 1', rate: '0' }, { name: 'Tax 2', rate: '0' }], items: [{ id: 'item-1', description: 'On-site assessment and project planning', quantity: '2.5', unitPrice: '199.95' }, { id: 'item-2', description: 'Equipment setup and testing', quantity: '3', unitPrice: '100.00' }] },
  'work-order': { ...sampleCommon, templateId: 'work-order', number: 'WO-2026-001', validUntil: '', pricing: false, notes: 'Check in at reception on arrival. Confirm access with the site contact.', terms: '', items: [{ id: 'item-1', description: 'Inspect workstations and document any issues', quantity: '8' }, { id: 'item-2', description: 'Install approved replacement parts', quantity: '3' }] },
  'service-report': { ...sampleCommon, templateId: 'service-report', number: 'SR-2026-001', validUntil: '', pricing: false, description: 'Completed workstation checks and installed the approved replacement parts.', notes: '', terms: '', findings: 'All eight workstations passed the final checks. Two cables showed signs of wear.', recommendations: 'Replace worn cables during the next planned maintenance visit.', nextSteps: 'Client to confirm a suitable date for the follow-up visit.', items: [{ id: 'item-1', description: 'Workstations inspected and tested', quantity: '8' }, { id: 'item-2', description: 'Replacement parts installed', quantity: '3' }] },
};
export function blankDocument(templateId: DocumentTemplateId): BusinessDocument {
  const data = structuredClone(documentExamples[templateId]);
  data.business = { name: '', contact: '' }; data.client = { name: '', contact: '' }; data.number = ''; data.date = new Date().toISOString().slice(0, 10); data.validUntil = '';
  for (const key of ['location', 'person', 'description', 'notes', 'terms', 'findings', 'recommendations', 'nextSteps'] as const) data[key] = '';
  data.items = [];
  return data;
}
