import asyncRetry from 'async-retry';
import Docker from 'dockerode';
import { humanId } from 'human-id';

let docker: Docker | null = null;

function getDockerConnection() {
  if (!docker) {
    docker = new Docker();
  }

  return docker;
}

const LOCAL_SERVICES = {
  server: 3001,
  clickhouse: 8123,
  emails: 6260,
  composition_federation_2: 3069,
  usage: 4001,
  schema: 6500,
  external_composition: 3012,
  mock_server: 3042,
  'otel-collector': 4318,
  workflows: 3014,
} as const;

export type KnownServices = keyof typeof LOCAL_SERVICES;

export function getCDNPort() {
  if (process.env.RUN_AGAINST_LOCAL_SERVICES === '1') {
    return 9000;
  }

  return 8083;
}

export function getAppBaseUrl() {
  if (process.env.RUN_AGAINST_LOCAL_SERVICES === '1') {
    return 'localhost:3000';
  }

  return 'localhost:8080';
}

export async function getServiceHost(
  serviceName: KnownServices,
  servicePort: number,
  localhost = true,
): Promise<string> {
  if (process.env.RUN_AGAINST_LOCAL_SERVICES === '1') {
    return `localhost:${LOCAL_SERVICES[serviceName]}`;
  }

  const actualHost = localhost ? 'localhost' : serviceName;
  const docker = getDockerConnection();
  const containers = await docker.listContainers({
    filters: JSON.stringify({
      label: [`com.docker.compose.service=${serviceName}`],
    }),
  });

  if (containers.length === 0) {
    throw new Error(`Failed to locate Docker container for service "${serviceName}"!`);
  }

  const container = containers[0];
  const ports = container.Ports || [];

  if (ports.length === 0) {
    throw new Error(
      `Container "${container.Id}" for service "${serviceName}" does not expose any ports!`,
    );
  }

  const publicPort = ports.find(p => p.PublicPort === servicePort);
  const privatePort = ports.find(p => p.PrivatePort === servicePort);

  if (!publicPort && !privatePort) {
    throw new Error(
      `Container "${container.Id}" for service "${serviceName}" does not expose port "${servicePort}"!`,
    );
  }

  if (publicPort) {
    return `${actualHost}:${publicPort.PublicPort}`;
  }

  if (privatePort) {
    console.warn(
      `Container "${container.Id}" (service: "${serviceName}") expose port "${servicePort}" as "${privatePort.PublicPort}", please consider to update your setup!`,
    );

    return `${actualHost}:${privatePort.PublicPort}`;
  }

  return `${actualHost}:${servicePort}`;
}

export function generateUnique() {
  return humanId({
    separator: '',
    adjectiveCount: 1,
    addAdverb: true,
    capitalize: false,
  });
}

export function assertNonNull<T>(
  value: T | null,
  message = 'Expected non-null value.',
): asserts value is T {
  if (value === null) {
    throw new Error(message);
  }
}

export function assertNonNullish<T>(
  value: T | null | undefined,
  message = 'Expected non-null value.',
): asserts value is T {
  if (value === null) {
    throw new Error(message);
  }
}

export async function pollForEmailVerificationLink(input: string | { email: string; now: number }) {
  const email = typeof input === 'string' ? input : input.email;
  const now = new Date(typeof input === 'string' ? Date.now() - 10_000 : input.now).toISOString();
  const url = new URL('http://localhost:3014/_history');
  url.searchParams.set('after', now);

  return await asyncRetry(
    async () => {
      const emails = await fetch(url.toString())
        .then(res => res.json())
        .then(emails =>
          emails.filter((e: any) => e.to === email && e.subject === 'Verify your email'),
        );

      if (emails.length === 0) {
        throw new Error('Could not find email');
      }

      // take the latest one
      const result = emails[emails.length - 1];

      const urlMatch = result.body.match(/href=\"(http:\/\/[^\s"]+)/);
      if (!urlMatch) throw new Error('No URL found in email');

      return new URL(urlMatch[1]);
    },
    {
      retries: 10,
      minTimeout: 1000,
      maxTimeout: 10000,
    },
  );
}
