import { Component, Directive, TemplateRef, computed, contentChild, input } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import type { Resource } from '@angular/core';
import { SpinnerComponent } from '@shared/ui/spinner/spinner.component';
import { AlertComponent } from '@shared/ui/alert/alert.component';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { RemoteData, fromResource, foldRemote } from '@shared/application/remote-data';

/* Slot markers. Put on <ng-template> children of <app-async>. */
@Directive({ selector: '[appAsyncLoaded]' })
export class AsyncLoadedDirective {
  constructor(public tpl: TemplateRef<{ $implicit: unknown }>) {}
}
@Directive({ selector: '[appAsyncLoading]' })
export class AsyncLoadingDirective {
  constructor(public tpl: TemplateRef<unknown>) {}
}
@Directive({ selector: '[appAsyncEmpty]' })
export class AsyncEmptyDirective {
  constructor(public tpl: TemplateRef<unknown>) {}
}
@Directive({ selector: '[appAsyncError]' })
export class AsyncErrorDirective {
  constructor(public tpl: TemplateRef<{ $implicit: Error | undefined; retry: () => void }>) {}
}

/**
 * Renders exactly ONE of loading / empty / error / loaded for a signal-based
 * resource (e.g. httpResource). Built on a RemoteData tagged union (see
 * core/remote-data.ts), so the states are mutually exclusive by construction —
 * the UI can never show two at once ("impossible states"). Unprovided slots
 * fall back to sensible defaults.
 */
@Component({
  selector: 'app-async',
  imports: [NgTemplateOutlet, SpinnerComponent, AlertComponent, ButtonComponent],
  template: `
    <div aria-live="polite" [attr.aria-busy]="rd().tag === 'Loading' ? 'true' : null">
    @switch (rd().tag) {
      @case ('Loading') {
        @if (loadingTpl()) { <ng-container [ngTemplateOutlet]="loadingTpl()!.tpl" /> }
        @else { <app-spinner /> }
      }
      @case ('Failure') {
        @if (errorTpl()) {
          <ng-container [ngTemplateOutlet]="errorTpl()!.tpl"
            [ngTemplateOutletContext]="{ $implicit: error(), retry: retry }" />
        } @else {
          <app-alert type="error">{{ errorText() }}</app-alert>
          <div style="margin-top:1rem">
            <app-button variant="secondary" (click)="retry()">{{ retryText() }}</app-button>
          </div>
        }
      }
      @case ('Empty') {
        @if (emptyTpl()) { <ng-container [ngTemplateOutlet]="emptyTpl()!.tpl" /> }
        @else { <p>{{ emptyText() }}</p> }
      }
      @case ('Success') {
        <ng-container [ngTemplateOutlet]="loadedTpl().tpl"
          [ngTemplateOutletContext]="{ $implicit: value() }" />
      }
    }
    </div>
  `,
})
export class AsyncComponent<T> {
  // Two ways to feed this component:
  //  [resource] — a raw httpResource (the common case), or
  //  [data]     — an already-combined RemoteData (e.g. from a store via map2).
  resource = input<Resource<T>>();
  data = input<RemoteData<Error | undefined, T>>();
  isEmpty = input<(v: T) => boolean>(() => false);

  // Shared/English component: copy lives behind language-agnostic inputs. Defaults are
  // localizable via $localize (source = default locale, currently nl); callers may override.
  errorText = input($localize`:@@async.error:Er ging iets mis bij het laden van de gegevens.`);
  retryText = input($localize`:@@async.retry:Opnieuw proberen`);
  emptyText = input($localize`:@@async.empty:Geen gegevens gevonden.`);

  loadedTpl = contentChild.required(AsyncLoadedDirective);
  loadingTpl = contentChild(AsyncLoadingDirective);
  emptyTpl = contentChild(AsyncEmptyDirective);
  errorTpl = contentChild(AsyncErrorDirective);

  // Single source of truth: the supplied RemoteData, or the resource projected into one.
  protected rd = computed<RemoteData<Error | undefined, T>>(() => {
    const data = this.data();
    if (data) return data;
    const r = this.resource();
    return r ? fromResource(r, this.isEmpty()) : { tag: 'Loading' };
  });

  // value/error are pulled out via the exhaustive fold — only Success carries a
  // value, only Failure carries an error, so these can't lie.
  protected value = computed(() =>
    foldRemote(this.rd(), { loading: () => undefined, empty: () => undefined, failure: () => undefined, success: (v) => v }),
  );
  protected error = computed(() =>
    foldRemote(this.rd(), { loading: () => undefined, empty: () => undefined, failure: (e) => e, success: () => undefined }),
  );

  retry = () => {
    const r = this.resource();
    if (r && 'reload' in r && typeof (r as { reload?: unknown }).reload === 'function') {
      (r as { reload: () => void }).reload();
    }
  };
}

/** Convenience: import this array to get the wrapper + all slot directives. */
export const ASYNC = [
  AsyncComponent, AsyncLoadedDirective, AsyncLoadingDirective,
  AsyncEmptyDirective, AsyncErrorDirective,
] as const;
