import { Inject, Injectable, Logger } from '@nestjs/common';
import { seal, unseal } from '@mosaicstack/auth';
import type { Db } from '@mosaicstack/db';
import { providerCredentials, eq, and } from '@mosaicstack/db';
import { DB } from '../database/database.module.js';
import type { ProviderCredentialSummaryDto } from './provider-credentials.dto.js';

@Injectable()
export class ProviderCredentialsService {
  private readonly logger = new Logger(ProviderCredentialsService.name);

  constructor(@Inject(DB) private readonly db: Db) {}

  /**
   * Encrypt and store (or update) a credential for the given user + provider.
   * Uses an upsert pattern: one row per (userId, provider).
   */
  async store(
    userId: string,
    provider: string,
    type: 'api_key' | 'oauth_token',
    value: string,
    metadata?: Record<string, unknown>,
  ): Promise<void> {
    const encryptedValue = seal(value);

    await this.db
      .insert(providerCredentials)
      .values({
        userId,
        provider,
        credentialType: type,
        encryptedValue,
        metadata: metadata ?? null,
      })
      .onConflictDoUpdate({
        target: [providerCredentials.userId, providerCredentials.provider],
        set: {
          credentialType: type,
          encryptedValue,
          metadata: metadata ?? null,
          updatedAt: new Date(),
        },
      });

    this.logger.log(`Credential stored for user=${userId} provider=${provider}`);
  }

  /**
   * Decrypt and return the plain-text credential value for the given user + provider.
   * Returns null if no credential is stored.
   */
  async retrieve(userId: string, provider: string): Promise<string | null> {
    const rows = await this.db
      .select()
      .from(providerCredentials)
      .where(
        and(eq(providerCredentials.userId, userId), eq(providerCredentials.provider, provider)),
      )
      .limit(1);

    if (rows.length === 0) return null;

    const row = rows[0]!;

    // Skip expired OAuth tokens
    if (row.expiresAt && row.expiresAt < new Date()) {
      this.logger.warn(`Credential for user=${userId} provider=${provider} has expired`);
      return null;
    }

    try {
      return unseal(row.encryptedValue);
    } catch (err) {
      this.logger.error(
        `Failed to decrypt credential for user=${userId} provider=${provider}`,
        err instanceof Error ? err.message : String(err),
      );
      return null;
    }
  }

  /**
   * Delete the stored credential for the given user + provider.
   */
  async remove(userId: string, provider: string): Promise<void> {
    await this.db
      .delete(providerCredentials)
      .where(
        and(eq(providerCredentials.userId, userId), eq(providerCredentials.provider, provider)),
      );

    this.logger.log(`Credential removed for user=${userId} provider=${provider}`);
  }

  /**
   * List all providers for which the user has stored credentials.
   * Never returns decrypted values.
   */
  async listProviders(userId: string): Promise<ProviderCredentialSummaryDto[]> {
    const rows = await this.db
      .select({
        provider: providerCredentials.provider,
        credentialType: providerCredentials.credentialType,
        expiresAt: providerCredentials.expiresAt,
        metadata: providerCredentials.metadata,
        createdAt: providerCredentials.createdAt,
        updatedAt: providerCredentials.updatedAt,
      })
      .from(providerCredentials)
      .where(eq(providerCredentials.userId, userId));

    return rows.map((row) => ({
      provider: row.provider,
      credentialType: row.credentialType,
      exists: true,
      expiresAt: row.expiresAt?.toISOString() ?? null,
      metadata: row.metadata as Record<string, unknown> | null,
      createdAt: row.createdAt.toISOString(),
      updatedAt: row.updatedAt.toISOString(),
    }));
  }
}
