import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  signal,
  type OnInit,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { firstValueFrom } from 'rxjs';
import {
  UserScopesService,
  type GrantUserScopeInput,
  type UserScopesPayload,
} from './user-scopes.service';

/**
 * The six scope kinds from ADR-0025's catalogue. Hardcoded here so
 * the SPA can dropdown them without importing from `shared-auth`
 * (which would couple the admin app to the BFF lib path). Drift gate
 * lives BFF-side; if the catalogue grows, this list grows in lockstep.
 */
const SCOPE_KINDS = [
  'self',
  'etablissement',
  'delegation',
  'region',
  'siege',
  'unrestricted',
] as const;

const VALUE_BEARING = new Set<string>(['etablissement', 'delegation', 'region']);

/**
 * Admin scope-management screen per [ADR-0026 PR 2b](https://git.unespace.com/julien/apf_portal/src/branch/main/docs/decisions/0026-person-user-portal-data-model.md).
 * Lists every UserScope row for the target user, lets the admin grant
 * a new one (kind dropdown + value + optional expiresAt), and revoke
 * existing ones inline.
 *
 * Reaches the BFF at `/api/admin/users/:oid/scopes`. The `:oid` URL
 * param is the affected user's Entra `oid` — same identifier the
 * `/admin/users` list page uses, so a "Manage scopes" link from that
 * list lands here without a lookup.
 *
 * A11y posture (per [ADR-0016](https://git.unespace.com/julien/apf_portal/src/branch/main/docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md)):
 *   - Form labels associated via `<label>` wrapping or `for`.
 *   - Live region for status/error messages (`role="status"`).
 *   - Touch targets respect 44×44 min (button padding in the SCSS).
 *   - Revoke buttons confirm via `window.confirm` to avoid accidental
 *     destructive action on a row's keyboard activation.
 */
@Component({
  selector: 'app-user-scopes-page',
  imports: [FormsModule, RouterLink],
  templateUrl: './user-scopes.html',
  styleUrl: './user-scopes.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserScopesPage implements OnInit {
  private readonly service = inject(UserScopesService);
  private readonly route = inject(ActivatedRoute);

  protected readonly scopeKinds = SCOPE_KINDS;
  protected readonly oid = signal('');
  protected readonly page = signal<UserScopesPayload | null>(null);
  protected readonly loading = signal(false);
  protected readonly error = signal<string | null>(null);

  // Form state for the "grant new scope" row.
  protected readonly newKind = signal<string>('self');
  protected readonly newValue = signal('');
  protected readonly newExpiresAt = signal('');
  protected readonly submitting = signal(false);
  protected readonly submitError = signal<string | null>(null);

  protected readonly needsValue = computed(() => VALUE_BEARING.has(this.newKind()));

  ngOnInit(): void {
    const oid = this.route.snapshot.paramMap.get('oid') ?? '';
    this.oid.set(oid);
    void this.fetch();
  }

  protected async fetch(): Promise<void> {
    this.loading.set(true);
    this.error.set(null);
    try {
      const page = await firstValueFrom(this.service.list(this.oid()));
      this.page.set(page);
    } catch (err) {
      this.error.set(this.translateError(err, 'Could not load scopes for this user.'));
      this.page.set(null);
    } finally {
      this.loading.set(false);
    }
  }

  protected async grant(): Promise<void> {
    if (this.submitting()) return;
    this.submitting.set(true);
    this.submitError.set(null);
    const payload: GrantUserScopeInput = {
      kind: this.newKind(),
      ...(this.needsValue() ? { value: this.newValue().trim() } : {}),
      ...(this.newExpiresAt() ? { expiresAt: toIso(this.newExpiresAt()) } : {}),
    };
    try {
      await firstValueFrom(this.service.grant(this.oid(), payload));
      // Reset the form (kind reverts to 'self' as the "least
      // privileged" default), then refresh the list to show the new
      // row.
      this.newKind.set('self');
      this.newValue.set('');
      this.newExpiresAt.set('');
      await this.fetch();
    } catch (err) {
      this.submitError.set(this.translateError(err, 'Could not grant the scope.'));
    } finally {
      this.submitting.set(false);
    }
  }

  protected async revoke(scopeId: string, label: string): Promise<void> {
    const ok = window.confirm(`Revoke scope "${label}"?`);
    if (!ok) return;
    try {
      await firstValueFrom(this.service.revoke(this.oid(), scopeId));
      await this.fetch();
    } catch (err) {
      this.error.set(this.translateError(err, 'Could not revoke the scope.'));
    }
  }

  protected formatScope(kind: string, value: string): string {
    return value === '' ? kind : `${kind}:${value}`;
  }

  protected formatTimestamp(iso: string | null): string {
    if (iso === null) return '—';
    try {
      return new Date(iso).toLocaleString();
    } catch {
      return iso;
    }
  }

  private translateError(err: unknown, fallback: string): string {
    if (
      typeof err === 'object' &&
      err !== null &&
      'error' in err &&
      typeof (err as { error?: unknown }).error === 'object' &&
      (err as { error: { message?: unknown } }).error.message !== undefined
    ) {
      const msg = (err as { error: { message?: unknown } }).error.message;
      if (typeof msg === 'string') return msg;
      if (Array.isArray(msg) && msg.every((m) => typeof m === 'string')) {
        return msg.join(' • ');
      }
    }
    const status = errorStatus(err);
    if (status === 403) return 'You do not have admin access on this session.';
    if (status === 404) return 'User not found — has this persona ever signed in or been seeded?';
    return fallback;
  }
}

function toIso(localValue: string): string {
  const d = new Date(localValue);
  return Number.isNaN(d.getTime()) ? localValue : d.toISOString();
}

function errorStatus(err: unknown): number | null {
  if (typeof err === 'object' && err !== null && 'status' in err) {
    const s = (err as { status: unknown }).status;
    return typeof s === 'number' ? s : null;
  }
  return null;
}
