import { Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js';
import type {
  IGetAliasesQuery,
  IGetAliasesResult,
  IInsertAliasQuery,
  IUpdateAliasActiveQuery,
} from '../types.js';

// Postgres unique_violation — raised by the partial-unique index that allows at
// most one active alias per (case-insensitive) address.
const PG_UNIQUE_VIOLATION = '23505';

const getAliases = sql<IGetAliasesQuery>`
  SELECT id, alias, owner_id, is_active, created_at, updated_at
    FROM accounter_schema.email_ingestion_alias_routing
  WHERE owner_id = ANY($ownerIds)
  ORDER BY alias ASC
`;

const insertAlias = sql<IInsertAliasQuery>`
  INSERT INTO accounter_schema.email_ingestion_alias_routing (alias, owner_id)
  VALUES ($alias, $ownerId)
  RETURNING id, alias, owner_id, is_active, created_at, updated_at
`;

const updateAliasActive = sql<IUpdateAliasActiveQuery>`
  UPDATE accounter_schema.email_ingestion_alias_routing
    SET is_active = $isActive
  WHERE id = $id
  RETURNING id, alias, owner_id, is_active, created_at, updated_at
`;

export type AliasMutationResult =
  | { success: true; alias: IGetAliasesResult }
  | { success: false; message: string };

function isUniqueViolation(err: unknown): boolean {
  return (
    typeof err === 'object' &&
    err !== null &&
    'code' in err &&
    (err as { code?: unknown }).code === PG_UNIQUE_VIOLATION
  );
}

/**
 * Manages alias→owner routing rows for v2 email ingestion.
 *
 * Writes go through TenantAwareDBClient so the `tenant_isolation_write` RLS
 * policy (`owner_id = get_current_business_id()`) is enforced as defense in
 * depth on top of the resolver's membership check. The request must therefore
 * be scoped to the target business (X-Business-Scope) — the same convention as
 * every other tenant write in the app.
 *
 * Reads use an explicit `owner_id` scope filter because the table's
 * `alias_resolution_select` policy is `USING (TRUE)` (alias resolution must work
 * before a tenant context exists), so RLS does not constrain SELECTs here.
 */
@Injectable({ scope: Scope.Operation })
export class EmailIngestionAliasProvider {
  constructor(private db: TenantAwareDBClient) {}

  async createAlias(alias: string, ownerId: string): Promise<AliasMutationResult> {
    try {
      const rows = await insertAlias.run({ alias, ownerId }, this.db);
      return { success: true, alias: rows[0] };
    } catch (err) {
      if (isUniqueViolation(err)) {
        // Generic message: the conflicting active alias may belong to another
        // tenant (the global unique index spans tenants); do not leak ownership.
        return { success: false, message: `Alias "${alias}" is already in use` };
      }
      throw err;
    }
  }

  async setAliasActive(id: string, isActive: boolean): Promise<AliasMutationResult> {
    try {
      const rows = await updateAliasActive.run({ id, isActive }, this.db);
      if (rows.length === 0) {
        return { success: false, message: 'Alias not found or not authorized' };
      }
      return { success: true, alias: rows[0] };
    } catch (err) {
      if (isUniqueViolation(err)) {
        return { success: false, message: 'An active alias already exists for this address' };
      }
      throw err;
    }
  }

  async listAliases(ownerIds: readonly string[]): Promise<IGetAliasesResult[]> {
    if (ownerIds.length === 0) {
      return [];
    }
    const rows = await getAliases.run({ ownerIds: [...ownerIds] }, this.db);
    return rows;
  }
}
