import { assertNever } from '@shared/kernel/fp';

/**
 * Pure upload domain (no Angular). Reusable across wizards: the host wizard folds
 * `UploadState` into its Model and delegates upload `Msg`s to `reduceUpload`.
 * Illegal states are unrepresentable by construction; the few transitions a union
 * can't express (channel↔upload exclusivity, single-file categories) are enforced
 * here in the reducer. Effects live in the shell (upload-shell.service.ts).
 */

export type UploadStatus =
  | { type: 'idle' }
  | { type: 'queued' }
  | { type: 'uploading'; progressPct: number }
  | { type: 'complete'; documentId: string }
  | { type: 'failed'; reason: string }
  | { type: 'deleting'; documentId: string } // carries the id so a failed delete can revert
  | { type: 'deleted' };

export interface Upload {
  localId: string; // client-generated UUID; the sync tag / status key
  categoryId: string;
  fileName: string;
  fileSizeMb: number;
  status: UploadStatus;
  backgroundSync: boolean;
}

export type DeliveryChannel = 'digital' | 'post';

export interface DocumentCategory {
  categoryId: string;
  label: string;
  description: string;
  required: boolean;
  acceptedTypes: string[];
  maxSizeMb: number;
  multiple: boolean;
  allowPostDelivery: boolean;
}

export interface UploadState {
  categories: DocumentCategory[];
  uploads: Upload[];
  deliveryChannel: Record<string, DeliveryChannel>; // categoryId → channel; default 'digital'
  rejections: Record<string, string>; // categoryId → client-side validation message (transient)
  backgroundSyncAvailable: boolean;
  categoriesError?: string;
}

export const initialUpload: UploadState = {
  categories: [],
  uploads: [],
  deliveryChannel: {},
  rejections: {},
  backgroundSyncAvailable: false,
};

export type UploadMsg =
  | { type: 'CategoriesLoaded'; categories: DocumentCategory[] }
  | { type: 'CategoriesLoadFailed'; reason: string }
  | { type: 'BackgroundSyncAvailability'; available: boolean }
  | { type: 'FileSelected'; categoryId: string; localId: string; fileName: string; fileSizeMb: number }
  | { type: 'FileRejected'; categoryId: string; reason: 'type' | 'size' | 'multiple' }
  | { type: 'UploadQueued'; localId: string; backgroundSync: boolean }
  | { type: 'UploadProgress'; localId: string; progressPct: number }
  | { type: 'UploadComplete'; localId: string; documentId: string }
  | { type: 'UploadFailed'; localId: string; reason: string }
  | { type: 'UploadRetried'; localId: string }
  | { type: 'UploadRemoved'; localId: string }
  | { type: 'UploadDeleteRequested'; localId: string; documentId: string }
  | { type: 'UploadDeleting'; localId: string }
  | { type: 'UploadDeleteComplete'; localId: string }
  | { type: 'UploadDeleteFailed'; localId: string; reason: string }
  | { type: 'DeliveryChannelChanged'; categoryId: string; channel: DeliveryChannel }
  | {
      type: 'BackgroundUploadsReturned';
      results: Array<{ localId: string } & ({ success: true; documentId: string } | { success: false; reason: string })>;
    };

const REJECTION_MESSAGES: Record<'type' | 'size' | 'multiple', string> = {
  type: $localize`:@@upload.reject.type:Dit bestandstype is niet toegestaan voor deze categorie.`,
  size: $localize`:@@upload.reject.size:Dit bestand is te groot.`,
  multiple: $localize`:@@upload.reject.multiple:U kunt voor deze categorie maar één bestand uploaden.`,
};

const ACTIVE: ReadonlyArray<UploadStatus['type']> = ['queued', 'uploading', 'complete'];

/** A required category is satisfied by an initiated upload OR a post-delivery choice. */
export function categorySatisfied(s: UploadState, categoryId: string): boolean {
  if (s.deliveryChannel[categoryId] === 'post') return true;
  return s.uploads.some((u) => u.categoryId === categoryId && ACTIVE.includes(u.status.type));
}

export function requiredCategoriesSatisfied(s: UploadState): boolean {
  return s.categories.filter((c) => c.required).every((c) => categorySatisfied(s, c.categoryId));
}

/**
 * FE format-validation (never authority — the server re-validates). Returns the
 * rejection reason for one file, or null if it passes the category's type/size.
 */
export function rejectReason(cat: DocumentCategory, file: { type: string; sizeMb: number }): 'type' | 'size' | null {
  if (cat.acceptedTypes.length > 0 && !cat.acceptedTypes.includes(file.type)) return 'type';
  if (cat.maxSizeMb > 0 && file.sizeMb > cat.maxSizeMb) return 'size';
  return null;
}

/** Map one upload's status, leaving the rest of the list untouched. */
function mapUpload(s: UploadState, localId: string, f: (u: Upload) => Upload): UploadState {
  return { ...s, uploads: s.uploads.map((u) => (u.localId === localId ? f(u) : u)) };
}

const find = (s: UploadState, localId: string) => s.uploads.find((u) => u.localId === localId);
const categoryOf = (s: UploadState, categoryId: string) => s.categories.find((c) => c.categoryId === categoryId);

export function reduceUpload(s: UploadState, m: UploadMsg): UploadState {
  switch (m.type) {
    case 'CategoriesLoaded': {
      // Categories can change with the answers (e.g. the diploma upload disappears once
      // DUO is chosen). Drop uploads + channel choices for categories no longer present.
      const ids = new Set(m.categories.map((c) => c.categoryId));
      const deliveryChannel: Record<string, DeliveryChannel> = {};
      for (const c of m.categories) deliveryChannel[c.categoryId] = s.deliveryChannel[c.categoryId] ?? 'digital';
      const uploads = s.uploads.filter((u) => ids.has(u.categoryId));
      return { ...s, categories: m.categories, uploads, deliveryChannel, categoriesError: undefined };
    }
    case 'CategoriesLoadFailed':
      return { ...s, categoriesError: m.reason };
    case 'BackgroundSyncAvailability':
      return { ...s, backgroundSyncAvailable: m.available };

    case 'FileSelected': {
      // Reject at dispatch: unknown category, or a category set to post-delivery.
      if (!categoryOf(s, m.categoryId) || s.deliveryChannel[m.categoryId] === 'post') return s;
      const cat = categoryOf(s, m.categoryId)!;
      // Single-file categories: a new selection replaces any existing upload.
      const uploads = cat.multiple ? s.uploads : s.uploads.filter((u) => u.categoryId !== m.categoryId);
      const next: Upload = {
        localId: m.localId,
        categoryId: m.categoryId,
        fileName: m.fileName,
        fileSizeMb: m.fileSizeMb,
        status: { type: 'queued' },
        backgroundSync: false,
      };
      const { [m.categoryId]: _cleared, ...rejections } = s.rejections;
      return { ...s, uploads: [...uploads, next], rejections };
    }
    case 'FileRejected':
      return { ...s, rejections: { ...s.rejections, [m.categoryId]: REJECTION_MESSAGES[m.reason] } };

    case 'UploadQueued':
      return mapUpload(s, m.localId, (u) => ({ ...u, status: { type: 'queued' }, backgroundSync: m.backgroundSync }));
    case 'UploadProgress':
      return mapUpload(s, m.localId, (u) => ({ ...u, status: { type: 'uploading', progressPct: m.progressPct } }));
    case 'UploadComplete':
      return mapUpload(s, m.localId, (u) => ({ ...u, status: { type: 'complete', documentId: m.documentId } }));
    case 'UploadFailed':
      return mapUpload(s, m.localId, (u) => ({ ...u, status: { type: 'failed', reason: m.reason } }));
    case 'UploadRetried':
      return mapUpload(s, m.localId, (u) => ({ ...u, status: { type: 'queued' } }));

    case 'UploadRemoved':
      return { ...s, uploads: s.uploads.filter((u) => u.localId !== m.localId) };

    case 'UploadDeleteRequested':
    case 'UploadDeleting':
      // Both mark the in-flight delete; keep the documentId so a failure can revert.
      return mapUpload(s, m.localId, (u) => {
        const documentId = u.status.type === 'complete' ? u.status.documentId
          : u.status.type === 'deleting' ? u.status.documentId : '';
        return { ...u, status: { type: 'deleting', documentId } };
      });
    case 'UploadDeleteComplete':
      return { ...s, uploads: s.uploads.filter((u) => u.localId !== m.localId) };
    case 'UploadDeleteFailed':
      return mapUpload(s, m.localId, (u) =>
        u.status.type === 'deleting' ? { ...u, status: { type: 'complete', documentId: u.status.documentId } } : u);

    case 'DeliveryChannelChanged': {
      const cat = categoryOf(s, m.categoryId);
      // Reject post for a category that doesn't permit it.
      if (m.channel === 'post' && (!cat || !cat.allowPostDelivery)) return s;
      const deliveryChannel = { ...s.deliveryChannel, [m.categoryId]: m.channel };
      // Switching to post removes that category's uploads; switching to digital starts clean.
      const uploads = s.uploads.filter((u) => u.categoryId !== m.categoryId);
      return { ...s, deliveryChannel, uploads };
    }

    case 'BackgroundUploadsReturned': {
      let next = s;
      for (const r of m.results) {
        next = mapUpload(next, r.localId, (u) => ({
          ...u,
          status: r.success ? { type: 'complete', documentId: r.documentId } : { type: 'failed', reason: r.reason },
        }));
      }
      return next;
    }

    default:
      return assertNever(m);
  }
}

/** The submit payload fragment: digital docs carry their documentId, post categories the channel. */
export function deliveryRefs(s: UploadState): Array<{ categoryId: string; channel: DeliveryChannel; documentId?: string }> {
  const refs: Array<{ categoryId: string; channel: DeliveryChannel; documentId?: string }> = [];
  for (const c of s.categories) {
    if (s.deliveryChannel[c.categoryId] === 'post') {
      refs.push({ categoryId: c.categoryId, channel: 'post' });
    } else {
      for (const u of s.uploads) {
        if (u.categoryId === c.categoryId && u.status.type === 'complete') {
          refs.push({ categoryId: c.categoryId, channel: 'digital', documentId: u.status.documentId });
        }
      }
    }
  }
  return refs;
}

/** Used by the shell to find what to poll on return: still-in-flight uploads. */
export const inFlight = (s: UploadState): Upload[] =>
  s.uploads.filter((u) => u.status.type === 'queued' || u.status.type === 'uploading');
