/**
 * Container registry for dashboards that need partition key validation
 * and createIfNotExists support.
 */

import { Container, PartitionKeyDefinition, type Database } from '@azure/cosmos';
import { getCosmosClient, getDatabase } from './client.js';
import type { ContainerConfig } from './types.js';

const _registry: Map<string, ContainerConfig> = new Map();
const _containerCache: Map<string, Container> = new Map();

/**
 * Register containers with their partition key configuration.
 * Call once at app startup before any getRegisteredContainer() calls.
 */
export function registerContainers(definitions: Record<string, ContainerConfig>): void {
  for (const [name, config] of Object.entries(definitions)) {
    _registry.set(name, config);
  }
}

/**
 * Get a container that was previously registered.
 * Throws if the container name is unknown.
 */
export function getRegisteredContainer(name: string): Container {
  if (!_registry.has(name)) {
    throw new Error(`Unknown container '${name}'. Valid: ${[..._registry.keys()].join(', ')}`);
  }

  let container = _containerCache.get(name);
  if (!container) {
    container = getDatabase().container(name);
    _containerCache.set(name, container);
  }
  return container;
}

/**
 * Create all registered containers if they don't exist.
 * Call from a seed script or on first deploy.
 */
export async function initializeAllContainers(): Promise<void> {
  const client = getCosmosClient();
  const dbId = process.env.COSMOS_DATABASE || 'lysnrai';
  const database = await createDatabaseSafe(client, dbId);

  for (const [name, config] of _registry.entries()) {
    await createContainerSafe(database, name, config);
  }
}

function sleep(ms: number): Promise<void> {
  return new Promise(resolve => globalThis.setTimeout(resolve, ms));
}

function isCosmosConflict(err: unknown): boolean {
  const e = err as { code?: number; statusCode?: number; message?: string } | null;
  if (!e) return false;
  if (e.code === 409 || e.statusCode === 409) return true;
  return (e.message || '').toLowerCase().includes('already exists');
}

function isCosmosNotFound(err: unknown): boolean {
  const e = err as { code?: number; statusCode?: number; message?: string } | null;
  if (!e) return false;
  if (e.code === 404 || e.statusCode === 404) return true;
  return (e.message || '').toLowerCase().includes('not found');
}

async function createDatabaseSafe(
  client: ReturnType<typeof getCosmosClient>,
  dbId: string
): Promise<Database> {
  try {
    const { database } = await client.databases.createIfNotExists({ id: dbId });
    return database;
  } catch (err) {
    // createIfNotExists is not atomic; concurrent create can race and throw a conflict.
    if (isCosmosConflict(err)) return client.database(dbId);
    throw err;
  }
}

function buildIndexingPolicy(config: ContainerConfig) {
  if (!config.compositeIndexes || config.compositeIndexes.length === 0) return undefined;
  return {
    indexingMode: 'consistent' as const,
    automatic: true,
    includedPaths: [{ path: '/*' }],
    compositeIndexes: config.compositeIndexes.map(group =>
      group.map(p => ({ path: p.path, order: p.order ?? 'ascending' }))
    ),
  };
}

/** Normalize composite indexes for order-insensitive equality (default = ascending). */
function normalizeComposites(composites: unknown): string {
  const arr = Array.isArray(composites) ? composites : [];
  return JSON.stringify(
    arr.map((group: unknown) =>
      (Array.isArray(group) ? group : []).map((p: { path?: string; order?: string }) => ({
        path: p.path,
        order: p.order ?? 'ascending',
      }))
    )
  );
}

/**
 * Reconcile composite indexes onto an EXISTING container. `createIfNotExists`
 * never updates an existing container's indexing policy, so we read the current
 * definition and replace it only when the composite indexes differ.
 */
async function ensureIndexingPolicy(
  database: Database,
  name: string,
  config: ContainerConfig
): Promise<void> {
  const desired = buildIndexingPolicy(config);
  if (!desired) return;
  const container = database.container(name);
  const { resource: def } = await container.read();
  if (!def) return;
  const current = (def.indexingPolicy ?? {}) as { compositeIndexes?: unknown };
  if (
    normalizeComposites(current.compositeIndexes) === normalizeComposites(desired.compositeIndexes)
  ) {
    return; // already in place
  }
  await container.replace({
    id: name,
    partitionKey: def.partitionKey as PartitionKeyDefinition,
    ...(def.defaultTtl != null && { defaultTtl: def.defaultTtl }),
    indexingPolicy: {
      ...(def.indexingPolicy ?? {}),
      indexingMode: 'consistent',
      automatic: true,
      includedPaths: def.indexingPolicy?.includedPaths ?? desired.includedPaths,
      compositeIndexes: desired.compositeIndexes,
    },
  });
}

async function createContainerSafe(
  database: Database,
  name: string,
  config: ContainerConfig
): Promise<void> {
  const indexingPolicy = buildIndexingPolicy(config);
  const payload = {
    id: name,
    partitionKey: {
      paths: [config.partitionKeyPath],
      kind: 'Hash',
    } as PartitionKeyDefinition,
    ...(config.defaultTtl != null && { defaultTtl: config.defaultTtl }),
    ...(indexingPolicy && { indexingPolicy }),
  };

  for (let attempt = 0; attempt < 3; attempt += 1) {
    try {
      await database.containers.createIfNotExists(payload);
      // createIfNotExists won't update an existing container's index policy.
      if (indexingPolicy) await ensureIndexingPolicy(database, name, config);
      return;
    } catch (err) {
      if (isCosmosConflict(err)) {
        // Container already existed — still reconcile its indexing policy (best-effort).
        if (indexingPolicy) {
          try {
            await ensureIndexingPolicy(database, name, config);
          } catch {
            /* reconciliation is best-effort; container is usable */
          }
        }
        return;
      }

      // Sometimes the database/container metadata isn't immediately visible after creation.
      if (isCosmosNotFound(err) && attempt < 2) {
        await sleep(250 * (attempt + 1));
        continue;
      }

      throw err;
    }
  }
}

/**
 * Reset the registry (useful for testing).
 * @internal
 */
export function _resetRegistry(): void {
  _registry.clear();
  _containerCache.clear();
}
