import { Injectable, inject } from '@angular/core';
import { UploadAdapter, XhrUploadRequest, XhrUploadHandle, UPLOAD_ABORTED } from './upload.adapter';
import { problemDetail } from '@shared/infrastructure/api-error';
import { UploadMsg, Upload } from './upload.machine';

/**
 * Transport seam (PRD §6): how upload bytes leave the browser. The shipped impl is
 * KeepaliveTransport (XHR for progress; poll-on-return covers the background gap).
 * A ServiceWorkerTransport would set `backgroundSyncAvailable = true` and survive a
 * closed tab — swapping it in touches only this interface.
 */
export interface UploadTransport {
  readonly backgroundSyncAvailable: boolean;
  send(req: XhrUploadRequest, onProgress: (pct: number) => void): XhrUploadHandle;
}

@Injectable({ providedIn: 'root' })
class KeepaliveTransport implements UploadTransport {
  private adapter = inject(UploadAdapter);
  readonly backgroundSyncAvailable = false; // no Service Worker registered in this POC
  send(req: XhrUploadRequest, onProgress: (pct: number) => void) {
    return this.adapter.xhrUpload(req, onProgress);
  }
}

type Dispatch = (m: UploadMsg) => void;

/**
 * Orchestrates upload effects: drives the transport and translates outcomes into
 * domain messages. Holds no UI state itself — the host wizard's reducer owns it.
 * Effects only; the reducer stays pure.
 */
@Injectable({ providedIn: 'root' })
export class UploadShellService {
  private transport: UploadTransport = inject(KeepaliveTransport);
  private adapter = inject(UploadAdapter);
  private inflight = new Map<string, () => void>(); // localId → cancel

  get backgroundSyncAvailable(): boolean {
    return this.transport.backgroundSyncAvailable;
  }

  /** Start (or retry) an upload: queue → progress → complete/failed. */
  upload(req: XhrUploadRequest, dispatch: Dispatch): void {
    dispatch({ type: 'UploadQueued', localId: req.localId, backgroundSync: this.backgroundSyncAvailable });
    const { done, cancel } = this.transport.send(req, (pct) =>
      dispatch({ type: 'UploadProgress', localId: req.localId, progressPct: pct }),
    );
    this.inflight.set(req.localId, cancel);
    done
      .then(({ documentId }) => dispatch({ type: 'UploadComplete', localId: req.localId, documentId }))
      .catch((e) => {
        if (e !== UPLOAD_ABORTED) {
          dispatch({ type: 'UploadFailed', localId: req.localId, reason: typeof e === 'string' ? e : '' });
        }
      })
      .finally(() => this.inflight.delete(req.localId));
  }

  /** Optimistic delete: mark deleting → complete (removed) / failed (reverted). */
  delete(localId: string, documentId: string, dispatch: Dispatch): void {
    dispatch({ type: 'UploadDeleting', localId });
    this.adapter
      .deleteDocument(documentId)
      .then(() => dispatch({ type: 'UploadDeleteComplete', localId }))
      .catch((e) => dispatch({ type: 'UploadDeleteFailed', localId, reason: problemDetail(e, '') }));
  }

  /** Cancel any in-flight XHRs (e.g. when a category switches to post-delivery). */
  cancel(localIds: string[]): void {
    for (const id of localIds) {
      this.inflight.get(id)?.();
      this.inflight.delete(id);
    }
  }

  /** Poll-on-return: ask the BFF which still-in-flight uploads have arrived. */
  async pollReturning(uploads: Upload[], dispatch: Dispatch): Promise<void> {
    const ids = uploads.map((u) => u.localId);
    if (ids.length === 0) return;
    const items = await this.adapter.status(ids);
    const results = items
      .filter((i) => i.status === 'complete' && i.documentId)
      .map((i) => ({ localId: i.localId, success: true as const, documentId: i.documentId! }));
    if (results.length > 0) dispatch({ type: 'BackgroundUploadsReturned', results });
  }
}
