import Decimal from 'decimal.js';
import { SQL, asc, desc, sql, AnyColumn, getTableName } from 'drizzle-orm';
import { Paginated, PaginationQueryDto } from '@wms/shared';
import { requireRequestContext } from '@/common/context/request-context';
import { ForbiddenError, ValidationError } from '@/common/errors/domain-error';

export const D = (v: Decimal | number | string | null | undefined): Decimal => new Decimal(v ?? 0);
export const ZERO = new Decimal(0);
export const isZero = (v: Decimal) => v.isZero();
export const gt = (a: Decimal, b: Decimal | number) => a.greaterThan(b);
export const gte = (a: Decimal, b: Decimal | number) => a.greaterThanOrEqualTo(b);
export const lt = (a: Decimal, b: Decimal | number) => a.lessThan(b);
export const min = (a: Decimal, b: Decimal) => (a.lessThan(b) ? a : b);
export const sum = (values: (Decimal | number | string)[]) => values.reduce<Decimal>((acc, v) => acc.plus(v), ZERO);

/** Format a decimal for API responses: fixed 4 dp, no exponent. */
export const toApi = (v: Decimal | null | undefined): string | null => (v == null ? null : v.toFixed(4));

/** Serialises Decimal instances inside a row/object tree to strings. */
export function serialize<T>(value: T): T {
  if (value instanceof Decimal) return value.toString() as unknown as T;
  if (value instanceof Date) return value as unknown as T;
  if (Array.isArray(value)) return value.map((v) => serialize(v)) as unknown as T;
  if (value && typeof value === 'object') {
    const out: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(value as Record<string, unknown>)) out[k] = serialize(v);
    return out as T;
  }
  return value;
}

export function todayIso(timezone = 'Asia/Kuwait', at: Date = new Date()): string {
  const parts = new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(at);
  const y = parts.find((p) => p.type === 'year')!.value;
  const m = parts.find((p) => p.type === 'month')!.value;
  const d = parts.find((p) => p.type === 'day')!.value;
  return `${y}-${m}-${d}`;
}

/** UTC instant at which a local calendar day starts in the given IANA timezone. */
export function localDayStart(dateIso: string, timezone = 'Asia/Kuwait'): Date {
  // Compute the offset of the zone at local midnight by probing the UTC midnight guess.
  const guess = new Date(`${dateIso}T00:00:00Z`);
  const parts = new Intl.DateTimeFormat('en-US', { timeZone: timezone, hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).formatToParts(guess);
  const get = (t: string) => Number(parts.find((p) => p.type === t)!.value);
  const localAsUtc = Date.UTC(get('year'), get('month') - 1, get('day'), get('hour') % 24, get('minute'));
  const offsetMs = localAsUtc - guess.getTime(); // zone offset (positive east of UTC)
  return new Date(guess.getTime() - offsetMs);
}

export function addDays(dateIso: string, days: number): string {
  const d = new Date(`${dateIso}T00:00:00Z`);
  d.setUTCDate(d.getUTCDate() + days);
  return d.toISOString().slice(0, 10);
}

export function daysBetween(fromIso: string, toIso: string): number {
  return Math.round((Date.parse(`${toIso}T00:00:00Z`) - Date.parse(`${fromIso}T00:00:00Z`)) / 86400000);
}

/** ISO week key, e.g. 2026-W41, with a configurable first day of week (0=Sun..6=Sat). */
export function weekKey(dateIso: string, weekStartsOn = 1): { key: string; start: string; end: string } {
  const d = new Date(`${dateIso}T00:00:00Z`);
  const dow = d.getUTCDay();
  const diff = (dow - weekStartsOn + 7) % 7;
  const start = new Date(d);
  start.setUTCDate(d.getUTCDate() - diff);
  const end = new Date(start);
  end.setUTCDate(start.getUTCDate() + 6);
  // Week number relative to the year of the week start
  const jan1 = new Date(Date.UTC(start.getUTCFullYear(), 0, 1));
  const weekNo = Math.floor((start.getTime() - jan1.getTime()) / (7 * 86400000)) + 1;
  return { key: `${start.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`, start: start.toISOString().slice(0, 10), end: end.toISOString().slice(0, 10) };
}

export function monthKey(dateIso: string): string {
  return dateIso.slice(0, 7);
}

/** Resolves sort "field:dir" against a whitelist of sortable columns. */
export function orderBy(sort: string | undefined, columns: Record<string, AnyColumn>, fallback: SQL | AnyColumn): (SQL | AnyColumn)[] {
  if (!sort) return [fallback];
  const [field, dir] = sort.split(':');
  const col = columns[field];
  if (!col) return [fallback];
  return [dir?.toLowerCase() === 'asc' ? asc(col) : desc(col)];
}

export function paginate<T>(items: T[], total: number, q: PaginationQueryDto): Paginated<T> {
  return { items: serialize(items), page: q.page, pageSize: q.pageSize, total };
}

export const offsetOf = (q: PaginationQueryDto) => (q.page - 1) * q.pageSize;

/** Throws unless the caller may act in the given warehouse. */
export function assertWarehouseAccess(warehouseId: string): void {
  const ctx = requireRequestContext();
  if (ctx.warehouseIds === 'ALL') return;
  if (!ctx.warehouseIds.includes(warehouseId)) throw new ForbiddenError(`No access to warehouse ${warehouseId}`, 'WAREHOUSE_ACCESS');
}

/** SQL filter limiting a query to the caller's accessible warehouses (undefined = no filter for ALL). */
export function warehouseScopeFilter(col: AnyColumn): SQL | undefined {
  const ctx = requireRequestContext();
  if (ctx.warehouseIds === 'ALL') return undefined;
  if (!ctx.warehouseIds.length) return sql`false`;
  return sql`${col} in (${sql.join(ctx.warehouseIds.map((id) => sql`${id}`), sql`, `)})`;
}

/** For storer-scoped callers (portal users, storer API keys) the storer is forced. */
export function resolveStorerScope(requested?: string | null): string | undefined {
  const ctx = requireRequestContext();
  if (ctx.storerId) {
    if (requested && requested !== ctx.storerId) throw new ForbiddenError('Storer scope mismatch', 'STORER_SCOPE');
    return ctx.storerId;
  }
  return requested ?? undefined;
}

export function requireStorerScope(requested?: string | null): string {
  const s = resolveStorerScope(requested);
  if (!s) throw new ValidationError('storerId is required');
  return s;
}

export const nowSql = sql`now()`;
export const toDateOrNull = (v?: string | null) => (v ? v : null);

export function chunk<T>(arr: T[], size: number): T[][] {
  const out: T[][] = [];
  for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
  return out;
}

export function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const out = {} as Pick<T, K>;
  for (const k of keys) if (k in obj) out[k] = obj[k];
  return out;
}

/**
 * Fully qualified column reference for use inside correlated sub-queries.
 * Drizzle drops the table qualifier when the outer query has a single FROM
 * table, which makes `${table.id}` resolve against the sub-query alias.
 */
export const qcol = (c: AnyColumn): SQL => sql.raw(`"${getTableName(c.table)}"."${c.name}"`);
