import { GraphQLError } from 'graphql';
import { Inject, Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import { ENVIRONMENT } from '../../../shared/tokens.js';
import type { Environment } from '../../../shared/types/index.js';
import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js';
import { decryptCredential, encryptCredential } from '../helpers/encryption.js';
import {
  DeelPayloadSchema,
  GreenInvoicePayloadSchema,
  type DeelPayload,
  type GreenInvoicePayload,
} from '../helpers/payload-schemas.js';
import {
  IDeleteProviderCredentialsQuery,
  IGetProviderCredentialPayloadQuery,
  IGetProviderStatusesQuery,
  IUpsertProviderCredentialsQuery,
} from '../types.js';

const PROVIDER_SCHEMAS = {
  green_invoice: GreenInvoicePayloadSchema,
  deel: DeelPayloadSchema,
} as const;

export type ProviderKey = keyof typeof PROVIDER_SCHEMAS;

const upsertProviderCredentials = sql<IUpsertProviderCredentialsQuery>`
  INSERT INTO accounter_schema.provider_credentials (owner_id, provider, payload)
  VALUES (accounter_schema.get_current_business_id(), $provider, $encrypted)
  ON CONFLICT (owner_id, provider) DO UPDATE SET payload = EXCLUDED.payload, updated_at = now()
  RETURNING updated_at;`;

const deleteProviderCredentials = sql<IDeleteProviderCredentialsQuery>`
  DELETE FROM accounter_schema.provider_credentials
  WHERE owner_id = accounter_schema.get_current_business_id()
    AND provider = $provider;`;

const getProviderStatuses = sql<IGetProviderStatusesQuery>`
  SELECT provider, updated_at
  FROM accounter_schema.provider_credentials
  WHERE owner_id = accounter_schema.get_current_business_id();`;

const getProviderCredentialPayload = sql<IGetProviderCredentialPayloadQuery>`
  SELECT payload
  FROM accounter_schema.provider_credentials
  WHERE owner_id = accounter_schema.get_current_business_id()
    AND provider = $provider;`;

@Injectable({ scope: Scope.Operation, global: true })
export class ProviderCredentialsProvider {
  constructor(
    private db: TenantAwareDBClient,
    @Inject(ENVIRONMENT) private env: Environment,
  ) {}

  private get encryptionKey(): string {
    const key = this.env.credentialsEncryptionKey;
    if (!key) {
      throw new GraphQLError('Credentials encryption key is not configured', {
        extensions: { code: 'INTERNAL_SERVER_ERROR' },
      });
    }
    return key;
  }

  async setCredentials(provider: ProviderKey, payload: unknown): Promise<Date | undefined> {
    const schema = PROVIDER_SCHEMAS[provider];
    const result = schema.safeParse(payload);
    if (!result.success) {
      throw new GraphQLError(`Invalid credentials for provider "${provider}"`, {
        extensions: { code: 'BAD_USER_INPUT', details: result.error.flatten() },
      });
    }

    const encrypted = encryptCredential(JSON.stringify(result.data), this.encryptionKey);

    const credentials = await upsertProviderCredentials.run({ provider, encrypted }, this.db);
    return credentials[0]?.updated_at;
  }

  async deleteCredentials(provider: ProviderKey): Promise<void> {
    await deleteProviderCredentials.run({ provider }, this.db);
  }

  async getProviderStatuses(): Promise<Array<{ provider: string; configuredAt: string }>> {
    const rows = await getProviderStatuses.run(undefined, this.db);
    return rows.map(row => ({
      provider: row.provider!,
      configuredAt: row.updated_at!.toISOString(),
    }));
  }

  async getGreenInvoiceCredentials(): Promise<GreenInvoicePayload | null> {
    return this._getDecrypted<GreenInvoicePayload>('green_invoice', GreenInvoicePayloadSchema);
  }

  async getDeelCredentials(): Promise<DeelPayload | null> {
    return this._getDecrypted<DeelPayload>('deel', DeelPayloadSchema);
  }

  private async _getDecrypted<T>(
    provider: ProviderKey,
    schema: { parse: (v: unknown) => T },
  ): Promise<T | null> {
    const rows = await getProviderCredentialPayload.run({ provider }, this.db);
    if (rows.length === 0) return null;
    try {
      const plaintext = decryptCredential(rows[0].payload!, this.encryptionKey);
      return schema.parse(JSON.parse(plaintext));
    } catch (err) {
      console.error(
        `[ProviderCredentialsProvider] Failed to decrypt/parse "${provider}" credentials: ${err instanceof Error ? err.message : String(err)}`,
      );
      throw new GraphQLError('Failed to retrieve provider credentials', {
        extensions: { code: 'INTERNAL_SERVER_ERROR' },
      });
    }
  }
}
