import { z } from 'zod';
import type {
  ComparisonMode,
  ComparisonRange,
  DateRange,
  ReportingFilterMeta,
  ReportingFilters,
} from '../domain/filters.js';
import { REPORTING_CHANNELS, REPORTING_COMPARISON, REPORTING_GROUP_BY } from '../domain/filters.js';

/** Server-side safety cap for page size (F-142 §7/§11). */
export const REPORTING_PAGE_SIZE_MAX = 200;
/** Default page size when omitted. */
export const REPORTING_PAGE_SIZE_DEFAULT = 50;

/**
 * Accepts a single ISO datetime string or an array (Fastify querystring yields
 * string vs array); missing -> undefined. Used for repeatable UUID params.
 */
function repeatableUuid() {
  return z.preprocess(
    (val: unknown) => (val === undefined ? undefined : Array.isArray(val) ? val : [val]),
    z.array(z.uuid()).optional(),
  );
}

/** Repeatable free-text enum (e.g. order `state`). */
function repeatableString() {
  return z.preprocess(
    (val: unknown) => (val === undefined ? undefined : Array.isArray(val) ? val : [val]),
    z.array(z.string().min(1).max(40)).optional(),
  );
}

/**
 * Shared reporting filter schema (F-142 §6). Validates + normalizes raw query
 * params into a `ReportingFilters` value. `from`/`to` use `[from,to)` semantics
 * (inclusive start, exclusive end); `from<to` is enforced.
 */
export const reportingFiltersSchema = z
  .object({
    from: z
      .string()
      .refine((s) => !Number.isNaN(Date.parse(s)), { message: 'Invalid ISO 8601 datetime' }),
    to: z
      .string()
      .refine((s) => !Number.isNaN(Date.parse(s)), { message: 'Invalid ISO 8601 datetime' }),
    compare: z.enum(REPORTING_COMPARISON).default('none'),
    channel: z.enum(REPORTING_CHANNELS).default('all'),
    storeId: repeatableUuid(),
    terminalId: repeatableUuid(),
    cashierId: repeatableUuid(),
    paymentMethodId: repeatableUuid(),
    productId: repeatableUuid(),
    categoryId: repeatableUuid(),
    brandId: repeatableUuid(),
    customerId: z.uuid().optional(),
    state: repeatableString(),
    groupBy: z.enum(REPORTING_GROUP_BY).optional(),
    page: z.coerce.number().int().min(1).default(1),
    pageSize: z
      .coerce.number()
      .int()
      .min(1)
      .max(REPORTING_PAGE_SIZE_MAX)
      .default(REPORTING_PAGE_SIZE_DEFAULT),
    sort: z.string().min(1).max(200).optional(),
  })
  .refine((v) => new Date(v.from) < new Date(v.to), {
    message: 'from must be before to',
  })
  .transform((v): ReportingFilters => ({
    range: { from: v.from, to: v.to },
    compare: v.compare,
    channel: v.channel,
    storeIds: v.storeId ?? [],
    terminalIds: v.terminalId ?? [],
    cashierIds: v.cashierId ?? [],
    paymentMethodIds: v.paymentMethodId ?? [],
    productIds: v.productId ?? [],
    categoryIds: v.categoryId ?? [],
    brandIds: v.brandId ?? [],
    customerId: v.customerId,
    state: v.state ?? [],
    groupBy: v.groupBy,
    page: v.page,
    pageSize: v.pageSize,
    sort: v.sort,
  }));

export type ReportingFiltersValue = z.output<typeof reportingFiltersSchema>;

/** Parse + validate raw query into ReportingFilters (throws ZodError). */
export function parseReportingFilters(query: unknown): ReportingFilters {
  return reportingFiltersSchema.parse(query);
}

/**
 * Previous-period range for a comparison mode (`[from,to)` semantics).
 * `null` for `none`. `previous_equal` mirrors the exact duration; `previous_calendar`
 * shifts back by the calendar days spanned, UTC-aligned.
 */
export function comparisonRange(
  range: DateRange,
  mode: ComparisonMode,
): ComparisonRange | null {
  if (mode === 'none') return null;
  const start = new Date(range.from).getTime();
  const end = new Date(range.to).getTime();
  const duration = end - start;

  if (mode === 'previous_equal') {
    return {
      from: new Date(start - duration).toISOString(),
      to: new Date(start).toISOString(),
    };
  }

  // previous_calendar: shift back by the whole calendar days spanned, UTC-aligned,
  // so a calendar period maps to the prior same-length calendar period.
  const spanDays = Math.max(1, Math.ceil(duration / (24 * 60 * 60 * 1000)));
  const prevStart = new Date(start - spanDays * 24 * 60 * 60 * 1000);
  prevStart.setUTCHours(0, 0, 0, 0);
  const prevEnd = new Date(prevStart);
  prevEnd.setUTCDate(prevEnd.getUTCDate() + spanDays);
  return { from: prevStart.toISOString(), to: prevEnd.toISOString() };
}

/** Single source of truth for the filter metadata served to clients. */
export const REPORTING_FILTER_META: ReportingFilterMeta = {
  filters: [
    {
      name: 'from',
      type: 'date',
      required: true,
      repeatable: false,
      description: 'Inclusive start datetime (ISO 8601 with UTC offset).',
    },
    {
      name: 'to',
      type: 'date',
      required: true,
      repeatable: false,
      description: 'Exclusive end datetime (ISO 8601 with UTC offset).',
    },
    {
      name: 'compare',
      type: 'enum',
      required: false,
      repeatable: false,
      options: [...REPORTING_COMPARISON],
      description: 'Date-comparison mode against a previous period.',
    },
    {
      name: 'channel',
      type: 'enum',
      required: false,
      repeatable: false,
      options: [...REPORTING_CHANNELS],
      description: 'Sales channel filter.',
    },
    {
      name: 'storeId',
      type: 'uuid',
      required: false,
      repeatable: true,
      description: 'One or more store UUIDs.',
    },
    {
      name: 'terminalId',
      type: 'uuid',
      required: false,
      repeatable: true,
      description: 'One or more POS terminal UUIDs.',
    },
    {
      name: 'cashierId',
      type: 'uuid',
      required: false,
      repeatable: true,
      description: 'Backoffice user (cashier) UUIDs.',
    },
    {
      name: 'paymentMethodId',
      type: 'uuid',
      required: false,
      repeatable: true,
      description: 'Payment method UUIDs.',
    },
    {
      name: 'productId',
      type: 'uuid',
      required: false,
      repeatable: true,
      description: 'Catalog product UUIDs.',
    },
    {
      name: 'categoryId',
      type: 'uuid',
      required: false,
      repeatable: true,
      description: 'Category UUIDs (resolved against the current catalog).',
    },
    {
      name: 'brandId',
      type: 'uuid',
      required: false,
      repeatable: true,
      description: 'Brand UUIDs.',
    },
    {
      name: 'customerId',
      type: 'uuid',
      required: false,
      repeatable: false,
      description: 'Single customer UUID (walk-in POS has no customer).',
    },
    {
      name: 'state',
      type: 'string',
      required: false,
      repeatable: true,
      options: [
        'PAID',
        'PROCESSING',
        'SHIPPED',
        'DELIVERED',
        'COMPLETED',
        'PARTIALLY_REFUNDED',
        'REFUNDED',
        'CANCELLED',
      ],
      description: 'Order states to include.',
    },
    {
      name: 'groupBy',
      type: 'enum',
      required: false,
      repeatable: false,
      options: [...REPORTING_GROUP_BY],
      description: 'Dimension to group report rows by.',
    },
    {
      name: 'page',
      type: 'string',
      required: false,
      repeatable: false,
      description: '1-indexed page number (integer).',
    },
    {
      name: 'pageSize',
      type: 'string',
      required: false,
      repeatable: false,
      description: `Page size (1..${REPORTING_PAGE_SIZE_MAX}).`,
    },
    {
      name: 'sort',
      type: 'string',
      required: false,
      repeatable: false,
      description: 'Sort expression, e.g. -revenue.',
    },
  ],
  comparison: {
    modes: [...REPORTING_COMPARISON],
    rangeBounds: 'inclusive_start_exclusive_end',
  },
  groupBy: [...REPORTING_GROUP_BY],
  pagination: {
    pageMin: 1,
    pageSizeMin: 1,
    pageSizeMax: REPORTING_PAGE_SIZE_MAX,
  },
  // F-142 §4 baseline. Future reports flip these to 'available' as the model supports them.
  dataAvailability: {
    grossSales: 'available',
    netSales: 'unavailable',
    discounts: 'available',
    tax: 'available',
    unitsSold: 'available',
    orders: 'available',
    customers: 'available',
    margin: 'unavailable',
    paymentMethod: 'unavailable',
    refunds: 'unavailable',
    shipping: 'unavailable',
  },
};
