import { sha256Base64 } from '../signing/hashing.js';

export const LIFECYCLE_TERMINAL_STATES = ['ready', 'failed', 'cancelled'] as const;
export const LIFECYCLE_NON_TERMINAL_STATES = ['pending', 'provisioning', 'migrating', 'retiring'] as const;
export const LIFECYCLE_STATES = [...LIFECYCLE_NON_TERMINAL_STATES, ...LIFECYCLE_TERMINAL_STATES] as const;
export const LIFECYCLE_MIGRATION_STATES = ['pending', 'migrated', 'failed'] as const;
export const LIFECYCLE_OPERATIONS = ['ensure_dev_database', 'ensure_prod_database', 'migrate_schema', 'retire_database'] as const;
export const ENVIRONMENT_CLASSES = ['dev', 'staging', 'prod'] as const;
export const DATABASE_ENGINES = ['postgres', 'snowflake', 'snowflake_postgres', 'lakebase'] as const;
export const DATABASE_LIFECYCLE_MANAGED_BY = 'database_lifecycle';

export type EnvironmentClass = (typeof ENVIRONMENT_CLASSES)[number];
export type DatabaseEngine = (typeof DATABASE_ENGINES)[number];
export type LifecycleOperation = (typeof LIFECYCLE_OPERATIONS)[number];
export type LifecycleTerminalState = (typeof LIFECYCLE_TERMINAL_STATES)[number];
export type LifecycleNonTerminalState = (typeof LIFECYCLE_NON_TERMINAL_STATES)[number];
export type LifecycleState = (typeof LIFECYCLE_STATES)[number];
export type LifecycleMigrationState = (typeof LIFECYCLE_MIGRATION_STATES)[number];

export type LifecycleErrorCode =
  | 'unsupported_provider_capability'
  | 'backend_locked'
  | 'policy_blocked'
  | 'credential_resolution_failed'
  | 'terraform_failed'
  | 'callback_failed';

export const CREDENTIAL_RESOLVERS = ['opa_local', 'aws_secrets_manager', 'gcp_secret_manager', 'vault', 'kubernetes_secret'] as const;
export type CredentialResolver = (typeof CREDENTIAL_RESOLVERS)[number];

export type CredentialRef = {
  resolver: CredentialResolver;
  ref: string;
  field?: string;
};

export type DatabaseRequirement = {
  logicalName: string;
  engine: DatabaseEngine;
  version?: string;
  sizing?: Record<string, unknown>;
  extensions?: readonly string[];
  replicaCount?: number;
  migrationDirectory?: string;
};

export type TerraformDatabaseBackend = {
  provisioner: 'terraform';
  provider: 'aws-rds' | 'snowflake' | 'databricks';
  stateBackend: 's3' | 'gcs' | 'azurerm' | 'local';
  remoteState: boolean;
  locking: boolean;
};

export type DatabaseBackend = TerraformDatabaseBackend;

export type EnvironmentProfile = {
  id: string;
  organizationId: string;
  environmentClass: EnvironmentClass;
  environmentName: string;
  opaAgentId: string;
  supportedOperations: LifecycleOperation[];
  supportedEngines: DatabaseEngine[];
  backend: DatabaseBackend;
};

type DatabaseBindingBase = {
  id: string;
  bindingKey: string;
  requirementKey: string;
  logicalName: string;
  applicationId: string;
  environmentClass: EnvironmentClass;
  environmentName: string;
  desiredSpecHash: string;
  migrationState: LifecycleMigrationState;
};

// A binding that has completed provisioning. The `ready` discriminant
// guarantees that the OPA returned both `connectionMetadata` and
// `credentialRefs`, so consumers can rely on those without null checks.
export type ReadyDatabaseBinding = DatabaseBindingBase & {
  lifecycleState: 'ready';
  connectionMetadata: Record<string, string | number | boolean>;
  credentialRefs: Record<string, CredentialRef>;
};

// A binding in any non-ready state. Connection metadata and credential refs
// may be present from a prior `ready` transition or absent if the binding
// never reached `ready`.
export type IncompleteDatabaseBinding = DatabaseBindingBase & {
  lifecycleState: Exclude<LifecycleState, 'ready'>;
  connectionMetadata?: Record<string, string | number | boolean>;
  credentialRefs?: Record<string, CredentialRef>;
};

export type DatabaseBinding = ReadyDatabaseBinding | IncompleteDatabaseBinding;

export function isReadyDatabaseBinding(binding: DatabaseBinding): binding is ReadyDatabaseBinding {
  return binding.lifecycleState === 'ready';
}

export type LifecycleRequest = {
  id: string;
  operation: LifecycleOperation;
  bindingKey: string;
  requirementKey: string;
  desiredSpecHash: string;
  state: LifecycleState;
};

export type TerraformModuleInput = {
  bindingKey: string;
  requirement: DatabaseRequirement;
  credentialRefs: Record<string, CredentialRef>;
  metadata: Record<string, string>;
};

export type TerraformModuleOutput = {
  connection: Record<string, string | number | boolean>;
  credentialRefs: Record<string, CredentialRef>;
  resourceKey: string;
};

export function computeRequirementKey(requirement: Pick<DatabaseRequirement, 'logicalName' | 'engine'>): string {
  return `${slugify(requirement.logicalName)}~${encodeURIComponent(requirement.logicalName)}:${requirement.engine}`;
}

export function computeBindingKey(input: {
  organizationId: string;
  applicationId: string;
  environmentClass: EnvironmentClass;
  environmentName: string;
  requirementKey: string;
}): string {
  return [input.organizationId, ...bindingKeySegments(input)].join(':');
}

export function computeLegacyBindingKeyWithoutOrganization(input: {
  applicationId: string;
  environmentClass: EnvironmentClass;
  environmentName: string;
  requirementKey: string;
}): string {
  return bindingKeySegments(input).join(':');
}

function bindingKeySegments(input: {
  applicationId: string;
  environmentClass: EnvironmentClass;
  environmentName: string;
  requirementKey: string;
}): string[] {
  return [
    input.applicationId,
    input.environmentClass,
    `${slugify(input.environmentName)}~${encodeURIComponent(input.environmentName)}`,
    input.requirementKey
  ];
}

export async function computeDesiredSpecHash(requirement: DatabaseRequirement): Promise<string> {
  return await sha256Base64(JSON.stringify(canonicalize(requirement)));
}

export function isTerminalLifecycleState(state: LifecycleState): state is LifecycleTerminalState {
  return (LIFECYCLE_TERMINAL_STATES as readonly string[]).includes(state);
}

export function redactLifecycleSecrets<T>(value: T): T {
  return redact(value) as T;
}

function slugify(value: string): string {
  return value
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '');
}

function canonicalize(value: unknown): unknown {
  if (Array.isArray(value)) {
    return value.map(canonicalize).sort((left, right) => codepointCompare(JSON.stringify(left), JSON.stringify(right)));
  }
  if (value && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value)
        .filter(([, entryValue]) => entryValue !== undefined)
        .sort(([left], [right]) => codepointCompare(left, right))
        .map(([key, entryValue]) => [key, canonicalize(entryValue)])
    );
  }
  return value;
}

function codepointCompare(left: string, right: string): number {
  if (left < right) {
    return -1;
  }
  if (left > right) {
    return 1;
  }
  return 0;
}

function redact(value: unknown, preserveCredentialRefs = false): unknown {
  if (Array.isArray(value)) {
    return value.map((entry) => redact(entry, preserveCredentialRefs));
  }
  if (value && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value).map(([key, entryValue]) => {
        if (isCredentialRefKey(key)) {
          return [key, redact(entryValue, true)];
        }
        if (!preserveCredentialRefs && SECRET_KEY_PATTERN.test(key)) {
          return [key, '[REDACTED]'];
        }
        return [key, redact(entryValue, preserveCredentialRefs)];
      })
    );
  }
  return value;
}

// Shared secret-key tripwire: keys whose name strongly suggests a raw secret
// value. Used by `redactLifecycleSecrets` to scrub logs/error reporting and by
// the server-side `assertNoRawCredentialMaterial` guard to refuse callbacks
// that violate the typed-CredentialRef contract. NOT a security boundary --
// callers must still send `Record<string, string | number | boolean>` for
// `connectionMetadata` and `CredentialRef` for `credentialRefs`; this catches
// obvious misuse only.
export const SECRET_KEY_PATTERN = /password|secret|token|private[_-]?key|dsn/i;

// Schemes whose URI form embeds credentials before `@`. Used as a value-side
// tripwire alongside `SECRET_KEY_PATTERN` -- callers should never send
// connection strings; the typed `connectionMetadata` shape is the contract.
export const CREDENTIAL_BEARING_DSN_SCHEMES = [
  'postgres',
  'postgresql',
  'mysql',
  'mariadb',
  'mongodb',
  'mongodb+srv',
  'redis',
  'rediss',
  'amqp',
  'amqps',
  'kafka'
] as const;

export function isSecretKey(key: string): boolean {
  return SECRET_KEY_PATTERN.test(key);
}

export function containsCredentialMaterial(value: string): boolean {
  if (DSN_WITH_INLINE_AUTH.test(value)) {
    return true;
  }
  if (CREDENTIAL_KV_PATTERN.test(value)) {
    return true;
  }
  if (JSON_ENCODED_SECRET_PATTERN.test(value)) {
    return true;
  }
  return false;
}

const DSN_SCHEME_GROUP = CREDENTIAL_BEARING_DSN_SCHEMES.join('|').replace(/\+/g, '\\+');
const DSN_WITH_INLINE_AUTH = new RegExp(`(?:${DSN_SCHEME_GROUP}):\\/\\/[^/@\\s:]+:[^/@\\s]+@`, 'i');
const CREDENTIAL_KV_PATTERN = /(?:password|pwd|token|secret|private[_-]?key|api[_-]?key)\s*=\s*[^;&\s"]+/i;
const JSON_ENCODED_SECRET_PATTERN = /["']\s*(?:password|secret|token|private[_-]?key|dsn)\s*["']\s*:/i;

function isCredentialRefKey(key: string): boolean {
  return /credential[_-]?ref/i.test(key);
}
