import { Inject, Injectable, NotFoundException, Optional } from '@nestjs/common';
import type { CapabilityProvider } from '@insignia/iios-contracts';
import { SandboxProvider } from './sandbox.provider';
import { HttpProvider } from './http.provider';
import { EmailProvider } from './email.provider';
import { SmtpProvider, smtpFallbackFromEnv, smtpIdentityFromConfig, smtpIdentityFromEnv, storageResolver } from './smtp.provider';
import { TwilioSmsProvider, type TwilioCreds } from './twilio-sms.provider';
import { credKeyFromEnv } from './secret-crypto';
import { ProviderCredentialService } from './provider-credential.service';
import { STORAGE_PORT, type StoragePort } from '../media/storage.port';

const DEFAULT_CHANNELS = ['WEBHOOK', 'EMAIL', 'SMS', 'WHATSAPP', 'PORTAL'];

/**
 * Maps a channelType to the provider that executes egress for it. Sandbox by
 * default; if `IIOS_PROVIDER_URL_<CHANNELTYPE>` is set, a real HttpProvider
 * overrides the sandbox for that channel (the "flip the binding" swap). Unknown
 * channels fail closed — no silent egress path.
 *
 * Precedence is registration ORDER (register() does Map.set → last wins). For EMAIL:
 * sandbox → HTTP EmailProvider (if URL set) → SMTP (if SMTP env set), so SMTP > HTTP > sandbox.
 */
@Injectable()
export class CapabilityProviderRegistry {
  private readonly byChannel = new Map<string, CapabilityProvider>();

  constructor(
    @Optional() @Inject(STORAGE_PORT) private readonly storage?: StoragePort,
    @Optional() private readonly credentials?: ProviderCredentialService,
  ) {
    for (const ch of DEFAULT_CHANNELS) this.register(new SandboxProvider([ch]));
    for (const ch of DEFAULT_CHANNELS) {
      const url = process.env[`IIOS_PROVIDER_URL_${ch}`];
      if (!url) continue;
      // EMAIL gets an email-shaped envelope provider; other channels use the generic HTTP one.
      this.register(ch === 'EMAIL' ? new EmailProvider(url) : new HttpProvider(ch, url));
    }
    // Real SMTP for EMAIL wins over the HTTP relay when configured (registered last). Attachments
    // resolve through the media StoragePort when one is bound (else attachments FAIL closed). A
    // tenant's own SMTP (BYO) is resolved per scope at send time and takes precedence over the env
    // identity; register the provider whenever EITHER the env SMTP or the credential store exists.
    const smtp = smtpIdentityFromEnv();
    const smtpCreds = credKeyFromEnv() && this.credentials ? this.credentials : undefined;
    if (smtp || smtpCreds) {
      const resolver = this.storage ? storageResolver(this.storage) : undefined;
      const credResolver = smtpCreds ? (scopeId: string) => smtpCreds.resolve(scopeId, 'SMTP').then(smtpIdentityFromConfig) : undefined;
      this.register(new SmtpProvider(smtp ?? undefined, smtpFallbackFromEnv() ?? undefined, undefined, resolver, credResolver));
    }
    // BYO SMS via each tenant's own Twilio creds (resolved per scope at send time). Registered
    // only when the platform key + credential store are present — else SMS stays on the sandbox.
    if (credKeyFromEnv() && this.credentials) {
      const creds = this.credentials;
      this.register(new TwilioSmsProvider((scopeId) => creds.resolve(scopeId, 'TWILIO_SMS') as Promise<TwilioCreds | null>));
    }
  }

  register(provider: CapabilityProvider): void {
    for (const ch of provider.channelTypes) this.byChannel.set(ch, provider);
  }

  forChannel(channelType: string): CapabilityProvider {
    const p = this.byChannel.get(channelType);
    if (!p) throw new NotFoundException(`no egress provider registered for channel: ${channelType}`);
    return p;
  }

  /** Ops view: which provider backs each channel. */
  bindings(): Record<string, string> {
    return Object.fromEntries([...this.byChannel.entries()].map(([ch, p]) => [ch, p.name]));
  }
}
