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;
// The environment axis (edit/preview/deployed) lives on the binding, so a
// single `ensure_database` covers what used to be split into
// ensure_dev_database / ensure_prod_database. `migrate_schema` and
// `retire_database` were always environment-agnostic.
export const LIFECYCLE_OPERATIONS = ['ensure_database', 'migrate_schema', 'retire_database'] as const;
// Mirrors the platform's view modes (proto api.v1.ViewMode: VIEW_MODE_EDIT /
// VIEW_MODE_PREVIEW / VIEW_MODE_DEPLOYED). Working-state migrations and the
// shared workspace database serve edit/preview; deploy-commit migrations and
// the deploy gate concern deployed.
export const LIFECYCLE_ENVIRONMENTS = ['edit', 'preview', 'deployed'] as const;
export const DATABASE_ENGINES = ['postgres', 'snowflake', 'snowflake_postgres', 'lakebase'] as const;
export const DATABASE_LIFECYCLE_MANAGED_BY = 'database_lifecycle';

// Agent capability tag keys. A lifecycle worker publishes these in the
// `tags` map of its agent registration (merged into — never replacing — the
// tag map the agent already publishes; the existing `profile` tag carries
// datatag coverage, same meaning it has for execution routing. Lifecycle
// environment/profile coverage is published as pairs so edit-only staging
// profiles do not imply deployed staging support. The server matches pending
// lifecycle requests against these at poll/claim time and gates task creation
// on "some active org agent supports this".
export const DATABASE_LIFECYCLE_TAG_OPERATIONS = 'databaseLifecycle:operations';
export const DATABASE_LIFECYCLE_TAG_ENGINES = 'databaseLifecycle:engines';
export const DATABASE_LIFECYCLE_TAG_ENVIRONMENT_PROFILES = 'databaseLifecycle:environmentProfiles';

export type LifecycleEnvironment = (typeof LIFECYCLE_ENVIRONMENTS)[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'
  | 'migration_failed'
  | 'callback_failed'
  // Server-side classification for a terminal callback that arrived for a
  // request whose desiredSpecHash no longer matches the binding (binding has
  // moved to a newer spec). The callback's metadata/state is discarded and
  // the request is cancelled rather than applied to the current binding.
  | 'stale_dispatch';

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;
};

type DatabaseBindingBase = {
  id: string;
  bindingKey: string;
  requirementKey: string;
  logicalName: string;
  applicationId: string;
  environment: LifecycleEnvironment;
  // The datatag key (Profile.key, e.g. 'staging' / 'production') this
  // binding serves. The same datatag can exist across environments, so the
  // pair (environment, profile) — not either alone — scopes a binding.
  profile: 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;
};

// Physical database instances back the M2 shared-RDS allocation pattern: dev-DB
// provisioning issues `CREATE DATABASE`/`CREATE ROLE` against a pre-existing
// physical database instance instead of spinning up a fresh RDS per binding.
// The control plane is the dumb org-scoped state store (registry + atomic
// capacity counter); ALL selection and provisioning logic lives in the worker.
export const PHYSICAL_DATABASE_INSTANCE_STATUSES = ['active', 'draining', 'retired'] as const;
export type PhysicalDatabaseInstanceStatus = (typeof PHYSICAL_DATABASE_INSTANCE_STATUSES)[number];

export type PhysicalDatabaseInstance = {
  id: string;
  organizationId: string;
  region: string;
  environment: LifecycleEnvironment;
  engine: DatabaseEngine; // postgres-only in V1
  endpoint: string;
  masterCredentialRef: CredentialRef;
  capacityMax: number;
  capacityUsed: number;
  status: PhysicalDatabaseInstanceStatus;
  metadata: Record<string, unknown>;
  created?: Date;
  updated?: Date;
};

// One forward-only SQL migration the server attaches to a dispatch
// payload so the lifecycle worker's migration runner can apply it after
// provisioning succeeds. `version` is the sort key + the primary key in
// the worker's in-DB `superblocks_schema_migrations` ledger; `filename`
// is recorded for diagnostics; `sql` is the raw multi-statement SQL.
// Matches `orchestrator/pkg/databaselifecycle/migrations.Migration`.
export type LifecycleMigration = {
  version: string;
  filename: string;
  sql: string;
};

// Canonical wire payload for a lifecycle dispatch sent from the server to a
// lifecycle worker. The worker's `DispatchPayload` struct in
// orchestrator/pkg/databaselifecycle/dispatch.go decodes this JSON shape;
// keys and order here are intentional.
//
// The server describes WHAT (binding identity, desired spec, migrations,
// connection/credential context); the worker owns HOW (Terraform modules,
// state backends, credential resolvers, shared physical database instances — all resolved from
// the worker's local config keyed by the payload's environment + profile).
//
// `migrations` is present iff the operation should consider migration
// state — `ensure_database` / `migrate_schema` dispatches carry an array
// (possibly empty); `retire_database` omits it. An omitted slice keeps the
// worker's `MigrationState` at the default "pending"; an empty slice means
// "vacuous truth, mark migrated"; a non-empty slice triggers the runner.
export type LifecycleDispatchPayload = {
  bindingKey: string;
  connectionMetadata?: Record<string, string | number | boolean>;
  desiredSpec: DatabaseRequirement;
  desiredSpecHash: string;
  environment: LifecycleEnvironment;
  migrations?: LifecycleMigration[];
  operation: LifecycleOperation;
  profile: string;
  requestId: string;
  resourceKey: string;
  runtimeCredentialRefs?: Record<string, CredentialRef>;
  migrationCredentialRefs?: Record<string, CredentialRef>;
};

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;
  environment: LifecycleEnvironment;
  profile: string;
  requirementKey: string;
}): string {
  return [input.organizationId, ...bindingKeySegments(input)].join(':');
}

export function computeLegacyBindingKeyWithoutOrganization(input: {
  applicationId: string;
  environment: LifecycleEnvironment;
  profile: string;
  requirementKey: string;
}): string {
  return bindingKeySegments(input).join(':');
}

function bindingKeySegments(input: {
  applicationId: string;
  environment: LifecycleEnvironment;
  profile: string;
  requirementKey: string;
}): string[] {
  return [input.applicationId, input.environment, `${slugify(input.profile)}~${encodeURIComponent(input.profile)}`, input.requirementKey];
}

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

// resource_key identifies the physical resource a binding maps to in
// customer infrastructure, and is the unit of locking inside the lifecycle
// worker. Distinct from binding_key (product identity in the control plane)
// because the worker derives infrastructure identity from it — shape
// mirrors planning doc §15.
export function computeResourceKey(input: {
  organizationId: string;
  applicationId: string;
  requirementKey: string;
  environment: LifecycleEnvironment;
  profile: string;
  actorScope?: string;
}): string {
  return [
    input.organizationId,
    input.applicationId,
    input.requirementKey,
    input.environment,
    `${slugify(input.profile)}~${encodeURIComponent(input.profile)}`,
    input.actorScope ?? 'default'
  ].join('/');
}

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);
}
