import { Injectable, computed, inject, resource } from '@angular/core';
import {
  ApiClient,
  DocumentCategoryDto,
  UploadStatusItemDto,
} from '@shared/infrastructure/api-client';
import { problemDetail } from '@shared/infrastructure/api-error';
import { currentScenario } from '@shared/infrastructure/scenario';
import { environment } from '../../../environments/environment';
import { DocumentCategory } from './upload.machine';

/** Answer-derived query params that affect which categories the server presents. */
export interface CategoryParams {
  diplomaHerkomst?: string;
  taalvaardigheid?: string;
}

/** One arrived/known status item from poll-on-return. */
export interface StatusItem {
  localId: string;
  status: string; // 'complete' | 'unknown'
  documentId?: string;
}

export interface XhrUploadRequest {
  localId: string;
  categoryId: string;
  wizardId: string;
  file: File;
}

/** A live upload: progress is reported via the callback; cancel aborts the XHR. */
export interface XhrUploadHandle {
  done: Promise<{ documentId: string }>;
  cancel: () => void;
}

/** Sentinel rejection so the caller can tell a user-cancel from a real failure. */
export const UPLOAD_ABORTED = Symbol('upload-aborted');

/**
 * Infrastructure: the only place upload HTTP lives. Reads (categories, status,
 * delete) go through the NSwag client; the multipart POST is hand-written XHR
 * because the generated client is JSON-only (the endpoint is ExcludeFromDescription)
 * and we need upload-progress events + cancellation, which fetch can't give us.
 */
@Injectable({ providedIn: 'root' })
export class UploadAdapter {
  private client = inject(ApiClient);

  /** Categories are server-owned; some depend on answers (e.g. registratie's diploma
      set varies by DUO/handmatig + course language). `extra` makes the resource reactive
      so it re-fetches when those answers change. */
  categoriesResource(wizardId: string, extra?: () => CategoryParams) {
    // Structural equality so we re-fetch only when a category-affecting answer changes,
    // not on every unrelated draft edit (a computed with `equal` returns the cached
    // reference while equal, so the resource sees no change).
    const params = computed(
      () => ({
        wizardId,
        diplomaHerkomst: extra?.().diplomaHerkomst,
        taalvaardigheid: extra?.().taalvaardigheid,
      }),
      {
        equal: (a, b) =>
          a.wizardId === b.wizardId &&
          a.diplomaHerkomst === b.diplomaHerkomst &&
          a.taalvaardigheid === b.taalvaardigheid,
      },
    );
    return resource({
      params,
      loader: ({ params }) =>
        this.client
          .categories(params.wizardId, params.diplomaHerkomst, params.taalvaardigheid)
          .then((r) => (r.categories ?? []).map(toCategory)),
    });
  }

  /** Poll-on-return: which client localIds have arrived at the BFF. */
  status(localIds: string[]): Promise<StatusItem[]> {
    if (localIds.length === 0) return Promise.resolve([]);
    return this.client.status(localIds.join(',')).then((r) => (r.results ?? []).map(toStatusItem));
  }

  /** User delete; throws a ProblemDetails (e.g. 409 once linked) the caller maps. */
  deleteDocument(documentId: string): Promise<void> {
    return this.client.uploads(documentId);
  }

  /**
   * Direct URL to the stored bytes, for a preview/download link (`<a href>`) — the
   * server serves it inline for pdf/image, attachment otherwise. Not a fetch: the
   * browser opens it. demo-* ids (dev simulation) have no bytes and 404 here.
   */
  contentUrl(documentId: string): string {
    return `${environment.apiBaseUrl}/api/v1/uploads/${documentId}/content`;
  }

  /**
   * Multipart upload with progress + cancel. ponytail: XHR (not fetch keepalive) —
   * progress events need XHR; the keepalive/background gap is covered by
   * poll-on-return (UploadShellService.pollReturning). Swap in a Service Worker
   * transport behind UploadTransport for true background sync.
   */
  xhrUpload(req: XhrUploadRequest, onProgress: (pct: number) => void): XhrUploadHandle {
    const scenario = currentScenario();
    if (scenario === 'upload-slow' || scenario === 'upload-fail')
      return simulateUpload(scenario, onProgress);

    const xhr = new XMLHttpRequest();
    const form = new FormData();
    form.append('file', req.file);
    form.append('categoryId', req.categoryId);
    form.append('localId', req.localId);
    form.append('wizardId', req.wizardId);

    let aborted = false;
    const done = new Promise<{ documentId: string }>((resolve, reject) => {
      xhr.upload.addEventListener('progress', (e) => {
        if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100));
      });
      xhr.addEventListener('load', () => {
        if (xhr.status >= 200 && xhr.status < 300) {
          try {
            resolve({ documentId: JSON.parse(xhr.responseText).documentId });
          } catch {
            reject(genericError());
          }
        } else {
          reject(parseError(xhr.responseText));
        }
      });
      xhr.addEventListener('error', () => reject(genericError()));
      xhr.addEventListener('abort', () =>
        aborted ? reject(UPLOAD_ABORTED) : reject(genericError()),
      );
    });

    xhr.open('POST', `${environment.apiBaseUrl}/api/v1/uploads`);
    xhr.send(form);
    return { done, cancel: () => ((aborted = true), xhr.abort()) };
  }
}

const UPLOAD_FAILED = $localize`:@@upload.failed:Uploaden is niet gelukt. Probeer het opnieuw.`;
const genericError = (): string => UPLOAD_FAILED;

/**
 * Demo-only (dev): the real XHR POST finishes instantly for metadata, so progress
 * and failure can't otherwise be shown. Drives the progress bar over ~2.5s, then
 * succeeds (`upload-slow`) or fails (`upload-fail`). ponytail: timer-based, cancel
 * via the returned handle; no network.
 */
function simulateUpload(
  scenario: 'upload-slow' | 'upload-fail',
  onProgress: (pct: number) => void,
): XhrUploadHandle {
  let pct = 0;
  let cancelled = false;
  const done = new Promise<{ documentId: string }>((resolve, reject) => {
    const tick = () => {
      if (cancelled) {
        reject(UPLOAD_ABORTED);
      } else if (pct < 100) {
        pct += 10;
        onProgress(Math.min(pct, 100));
        setTimeout(tick, 250);
      } else if (scenario === 'upload-fail') {
        reject(UPLOAD_FAILED);
      } else {
        resolve({ documentId: `demo-${crypto.randomUUID()}` });
      }
    };
    setTimeout(tick, 250);
  });
  return { done, cancel: () => void (cancelled = true) };
}
const parseError = (body: string): string => {
  try {
    return problemDetail(JSON.parse(body), UPLOAD_FAILED);
  } catch {
    return genericError();
  }
};

/** Wire DTO (all fields optional) → domain. */
function toCategory(c: DocumentCategoryDto): DocumentCategory {
  return {
    categoryId: c.categoryId ?? '',
    label: c.label ?? '',
    description: c.description ?? '',
    required: c.required ?? false,
    acceptedTypes: c.acceptedTypes ?? [],
    maxSizeMb: c.maxSizeMb ?? 0,
    multiple: c.multiple ?? false,
    allowPostDelivery: c.allowPostDelivery ?? false,
  };
}

function toStatusItem(s: UploadStatusItemDto): StatusItem {
  return {
    localId: s.localId ?? '',
    status: s.status ?? 'unknown',
    documentId: s.documentId ?? undefined,
  };
}
