import { Injectable, signal } from '@angular/core';
import { Observable, of, throwError } from 'rxjs';
import { PartnerCredential, RegisterCredentialRequest } from '../models/partner-credential.model';
import {
  CreateNodeRequest,
  Environment,
  NodeLevel,
  NodeStatus,
  ProvisioningNode,
} from '../models/provisioning-node.model';
import { PartnerHierarchyGateway } from './partner-hierarchy-gateway.interface';

/**
 * In-memory hierarchy for local dev, until the partner API ships.
 *
 * Enforces the invariants the real backend enforces (§2), rather than being a
 * permissive stub: externalReference uniqueness, terminal `disabled`,
 * cascading disable, no re-parenting. A mock that accepts what the real
 * backend rejects teaches the UI the wrong lesson.
 */

const NOW = '2026-08-18T00:00:00.000Z';

function node(
  id: string,
  level: NodeLevel,
  parentId: string | null,
  companyId: string,
  path: string[],
  displayName: string,
  externalReference: string,
  status: NodeStatus = 'active',
): ProvisioningNode {
  return {
    id, level, parentId, companyId, path, displayName, externalReference,
    environment: 'TEST', status, createdAt: NOW, updatedAt: NOW,
  };
}

const SEED: ProvisioningNode[] = [
  node('cmp_1', 'company', null, 'cmp_1', ['cmp_1'], 'Demo Company', 'demo-co'),
  node('prj_1', 'project', 'cmp_1', 'cmp_1', ['cmp_1', 'prj_1'], 'marketplaces', 'demo-marketplaces'),
  node('str_1', 'store', 'prj_1', 'cmp_1', ['cmp_1', 'prj_1', 'str_1'], 'Demo Storefront', 'demo-store-1'),
  node('pp_1', 'payment_point', 'str_1', 'cmp_1', ['cmp_1', 'prj_1', 'str_1', 'pp_1'], 'QR acceptance', 'demo-pp-qr'),
  node('pp_2', 'payment_point', 'str_1', 'cmp_1', ['cmp_1', 'prj_1', 'str_1', 'pp_2'], 'Card acceptance', 'demo-pp-card'),
];

@Injectable({ providedIn: 'root' })
export class PartnerHierarchyLocalGateway implements PartnerHierarchyGateway {
  private readonly nodes = signal<ProvisioningNode[]>([...SEED]);
  private readonly credentials = signal<PartnerCredential[]>([]);
  private sequence = 1;

  loadHierarchy(companyId: string, environment: Environment): Observable<ProvisioningNode[]> {
    return of(this.nodes().filter(n => n.companyId === companyId && n.environment === environment));
  }

  loadNode(nodeId: string): Observable<ProvisioningNode | null> {
    return of(this.nodes().find(n => n.id === nodeId) ?? null);
  }

  lookupByExternalReference(
    externalReference: string,
    environment: Environment,
  ): Observable<ProvisioningNode | null> {
    return of(
      this.nodes().find(
        n => n.externalReference === externalReference && n.environment === environment,
      ) ?? null,
    );
  }

  createProject(companyId: string, request: CreateNodeRequest): Observable<ProvisioningNode> {
    return this.create(companyId, 'project', request);
  }

  createStore(projectId: string, request: CreateNodeRequest): Observable<ProvisioningNode> {
    return this.create(projectId, 'store', request);
  }

  createPaymentPoint(storeId: string, request: CreateNodeRequest): Observable<ProvisioningNode> {
    return this.create(storeId, 'payment_point', request);
  }

  setStatus(
    nodeId: string,
    status: Extract<NodeStatus, 'active' | 'suspended'>,
  ): Observable<ProvisioningNode> {
    const existing = this.nodes().find(n => n.id === nodeId);
    if (!existing) {
      return throwError(() => new Error(`Node not found: ${nodeId}`));
    }
    if (existing.status === 'disabled') {
      // §2: disabled is terminal. The mock must refuse this too, or the UI
      // learns a transition the real backend will reject.
      return throwError(() => new Error('A disabled node cannot change status.'));
    }

    const updated = { ...existing, status, updatedAt: new Date().toISOString() };
    this.nodes.update(all => all.map(n => (n.id === nodeId ? updated : n)));
    return of(updated);
  }

  disable(nodeId: string): Observable<ProvisioningNode> {
    const existing = this.nodes().find(n => n.id === nodeId);
    if (!existing) {
      return throwError(() => new Error(`Node not found: ${nodeId}`));
    }

    // §2: disabling cascades to every descendant, atomically.
    const updatedAt = new Date().toISOString();
    this.nodes.update(all =>
      all.map(n =>
        n.id === nodeId || n.path.includes(nodeId)
          ? { ...n, status: 'disabled' as const, updatedAt }
          : n,
      ),
    );
    return of({ ...existing, status: 'disabled' as const, updatedAt });
  }

  loadCredentials(companyId: string): Observable<PartnerCredential[]> {
    const companyNodeIds = new Set(
      this.nodes().filter(n => n.companyId === companyId).map(n => n.id),
    );
    return of(this.credentials().filter(c => companyNodeIds.has(c.scopeNodeId)));
  }

  registerCredential(request: RegisterCredentialRequest): Observable<PartnerCredential> {
    const credential: PartnerCredential = {
      partnerId: 'ptr_local',
      keyId: `key_${this.sequence++}`,
      scopeNodeId: request.scopeNodeId,
      environment: request.environment,
      algorithm: request.algorithm,
      publicKey: request.publicKey,
      status: 'active',
      createdAt: new Date().toISOString(),
    };
    this.credentials.update(all => [...all, credential]);
    return of(credential);
  }

  rotateCredential(keyId: string, newPublicKey: string): Observable<PartnerCredential> {
    const existing = this.credentials().find(c => c.keyId === keyId);
    if (!existing) {
      return throwError(() => new Error(`Credential not found: ${keyId}`));
    }
    if (existing.status === 'revoked') {
      return throwError(() => new Error('A revoked credential cannot be rotated.'));
    }

    const updated: PartnerCredential = { ...existing, publicKey: newPublicKey, status: 'rotating' };
    this.credentials.update(all => all.map(c => (c.keyId === keyId ? updated : c)));
    return of(updated);
  }

  revokeCredential(keyId: string): Observable<void> {
    this.credentials.update(all =>
      all.map(c => (c.keyId === keyId ? { ...c, status: 'revoked' as const } : c)),
    );
    return of(undefined);
  }

  private create(
    parentId: string,
    level: NodeLevel,
    request: CreateNodeRequest,
  ): Observable<ProvisioningNode> {
    const parent = this.nodes().find(n => n.id === parentId);
    if (!parent) {
      return throwError(() => new Error(`Parent not found: ${parentId}`));
    }

    // §2 invariant 3: externalReference is unique per (companyId, environment, level).
    const collision = this.nodes().some(
      n =>
        n.companyId === parent.companyId &&
        n.environment === parent.environment &&
        n.level === level &&
        n.externalReference === request.externalReference,
    );
    if (collision) {
      return throwError(
        () => new Error(`externalReference already in use at this level: ${request.externalReference}`),
      );
    }

    const id = `${level}_${this.sequence++}`;
    const created: ProvisioningNode = {
      id,
      level,
      parentId,
      companyId: parent.companyId,
      // §2 invariant 4: path is server-computed, never client-supplied.
      path: [...parent.path, id],
      // §2 invariant 2: environment is inherited and immutable.
      environment: parent.environment,
      status: 'active',
      externalReference: request.externalReference,
      displayName: request.displayName,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };

    this.nodes.update(all => [...all, created]);
    return of(created);
  }
}
