import * as plugins from '../plugins.js';
import { logger } from '../logging.js';
import { getErrorMessage } from '../utils/error.js';
import { OneboxDatabase } from '../database/index.js';

export type TDcRouterMode = 'managed' | 'external' | 'disabled';

export interface IManagedDcRouterStatus {
  mode: TDcRouterMode;
  configured: boolean;
  running: boolean;
  healthy: boolean;
  containerId?: string;
  image: string;
  gatewayUrl: string;
  opsPort: number;
  httpPort: number;
  httpsPort: number;
  message?: string;
}

const containerName = 'onebox-dcrouter';
const defaultImage = 'code.foss.global/serve.zone/dcrouter:latest';
const defaultDataDir = './.nogit/dcrouter-data';
const defaultOpsPort = 3300;
const defaultHttpPort = 80;
const defaultHttpsPort = 443;
const internalBaseDir = '/data';

export class ManagedDcRouterManager {
  private database: OneboxDatabase;
  private dockerClient: InstanceType<typeof plugins.docker.Docker> | null = null;

  constructor(private oneboxRef: any) {
    this.database = oneboxRef.database;
  }

  public async getMode(): Promise<TDcRouterMode> {
    const storedMode = await this.database.getSetting('dcrouterMode');
    if (storedMode === 'managed' || storedMode === 'external' || storedMode === 'disabled') {
      return storedMode;
    }

    const hasExternalGateway = Boolean(await this.database.getSetting('dcrouterGatewayUrl'));
    return hasExternalGateway ? 'external' : 'managed';
  }

  public async getImage(): Promise<string> {
    return await this.database.getSetting('dcrouterManagedImage') || defaultImage;
  }

  public async getOpsPort(): Promise<number> {
    return this.parsePort(await this.database.getSetting('dcrouterManagedOpsPort'), defaultOpsPort);
  }

  public async getHttpPort(): Promise<number> {
    return this.parsePort(await this.database.getSetting('dcrouterManagedHttpPort'), defaultHttpPort);
  }

  public async getHttpsPort(): Promise<number> {
    return this.parsePort(await this.database.getSetting('dcrouterManagedHttpsPort'), defaultHttpsPort);
  }

  public async getDataDir(): Promise<string> {
    return await this.database.getSetting('dcrouterManagedDataDir') || defaultDataDir;
  }

  public async getGatewayUrl(): Promise<string> {
    return `http://127.0.0.1:${await this.getOpsPort()}`;
  }

  public getRouteTarget(): { host: string; port: number } {
    return {
      host: 'onebox-coretraffic',
      port: 80,
    };
  }

  public async ensureGatewayClientId(): Promise<string> {
    let gatewayClientId = await this.database.getSetting('dcrouterGatewayClientId');
    if (!gatewayClientId) {
      gatewayClientId = `onebox-${crypto.randomUUID()}`;
      await this.database.setSetting('dcrouterGatewayClientId', gatewayClientId);
    }
    return gatewayClientId;
  }

  public async getAdminToken(): Promise<string> {
    const existingToken = await this.database.getSecretSetting('dcrouterManagedAdminApiToken');
    if (existingToken) {
      return existingToken;
    }

    const token = `dcr_${crypto.randomUUID().replaceAll('-', '')}${crypto.randomUUID().replaceAll('-', '')}`;
    await this.database.setSecretSetting('dcrouterManagedAdminApiToken', token);
    return token;
  }

  public async prepareGatewaySettings(): Promise<void> {
    if (await this.getMode() !== 'managed') {
      return;
    }

    const target = this.getRouteTarget();
    await this.database.setSetting('dcrouterMode', 'managed');
    await this.database.setSetting('dcrouterGatewayUrl', await this.getGatewayUrl());
    await this.database.setSetting('dcrouterTargetHost', target.host);
    await this.database.setSetting('dcrouterTargetPort', String(target.port));
    await this.ensureGatewayClientId();
    await this.getAdminToken();
  }

  public async init(): Promise<void> {
    if (await this.getMode() === 'managed') {
      await this.start();
      return;
    }

    await this.stop();
  }

  public async start(options: { recreate?: boolean } = {}): Promise<IManagedDcRouterStatus> {
    if (await this.getMode() !== 'managed') {
      throw new Error('Managed dcrouter mode is not enabled');
    }

    await this.prepareGatewaySettings();
    await this.ensureDockerClient();

    if (options.recreate) {
      await this.removeExistingContainer();
    }

    const existingContainer = await this.getExistingContainer();
    if (existingContainer) {
      if (this.isContainerRunning(existingContainer)) {
        await this.waitForReady().catch((error) => {
          logger.warn(`Managed dcrouter readiness check failed: ${getErrorMessage(error)}`);
        });
        return await this.getStatus();
      }

      await this.startContainer(existingContainer.Id);
      await this.waitForReady();
      return await this.getStatus();
    }

    await this.createContainer();
    await this.waitForReady();
    return await this.getStatus();
  }

  public async stop(): Promise<IManagedDcRouterStatus> {
    await this.ensureDockerClient();
    const existingContainer = await this.getExistingContainer();
    if (existingContainer && this.isContainerRunning(existingContainer)) {
      await this.stopContainer(existingContainer.Id);
    }
    return await this.getStatus();
  }

  public async restart(): Promise<IManagedDcRouterStatus> {
    return await this.start({ recreate: true });
  }

  public async getStatus(): Promise<IManagedDcRouterStatus> {
    const baseStatus: IManagedDcRouterStatus = {
      mode: await this.getMode(),
      configured: await this.getMode() === 'managed',
      running: false,
      healthy: false,
      image: await this.getImage(),
      gatewayUrl: await this.getGatewayUrl(),
      opsPort: await this.getOpsPort(),
      httpPort: await this.getHttpPort(),
      httpsPort: await this.getHttpsPort(),
    };

    try {
      await this.ensureDockerClient();
      const existingContainer = await this.getExistingContainer();
      if (!existingContainer) {
        return baseStatus;
      }

      const running = this.isContainerRunning(existingContainer);
      return {
        ...baseStatus,
        running,
        healthy: running ? await this.checkHealthy() : false,
        containerId: existingContainer.Id,
      };
    } catch (error) {
      return {
        ...baseStatus,
        message: getErrorMessage(error),
      };
    }
  }

  private async ensureDockerClient(): Promise<void> {
    if (!this.dockerClient) {
      this.dockerClient = new plugins.docker.Docker({
        socketPath: 'unix:///var/run/docker.sock',
      });
      await this.dockerClient.start();
    }
  }

  private parsePort(value: string | null, fallback: number): number {
    if (!value) return fallback;
    const port = Number(value);
    if (!Number.isInteger(port) || port < 1 || port > 65535) {
      return fallback;
    }
    return port;
  }

  private async getAbsoluteDataDir(): Promise<string> {
    const dataDir = plugins.path.resolve(await this.getDataDir());
    await plugins.fs.mkdir(dataDir, { recursive: true });
    return dataDir;
  }

  private async createContainer(): Promise<void> {
    const image = await this.getImage();
    const token = await this.getAdminToken();
    const dataDir = await this.getAbsoluteDataDir();
    const httpPort = await this.getHttpPort();
    const httpsPort = await this.getHttpsPort();
    const opsPort = await this.getOpsPort();
    await this.writeManagedConfig(dataDir);

    await this.oneboxRef.docker.pullImage(image);

    const response = await this.dockerClient!.request('POST', `/containers/create?name=${containerName}`, {
      Image: image,
      Env: [
        `DCROUTER_BASE_DIR=${internalBaseDir}`,
        `DCROUTER_CONFIG_PATH=${internalBaseDir}/managed-config.json`,
        `DCROUTER_ADMIN_API_TOKEN=${token}`,
        'DCROUTER_ADMIN_API_TOKEN_NAME=Onebox Managed Admin Token',
      ],
      Labels: {
        'managed-by': 'onebox',
        'onebox-type': 'dcrouter',
      },
      ExposedPorts: {
        '80/tcp': {},
        '443/tcp': {},
        '3000/tcp': {},
      },
      HostConfig: {
        NetworkMode: 'onebox-network',
        RestartPolicy: {
          Name: 'unless-stopped',
        },
        Binds: [`${dataDir}:${internalBaseDir}`],
        PortBindings: {
          '80/tcp': [{ HostIp: '0.0.0.0', HostPort: String(httpPort) }],
          '443/tcp': [{ HostIp: '0.0.0.0', HostPort: String(httpsPort) }],
          '3000/tcp': [{ HostIp: '127.0.0.1', HostPort: String(opsPort) }],
        },
      },
    });

    if (response.statusCode >= 300) {
      throw new Error(`Failed to create managed dcrouter container: HTTP ${response.statusCode} - ${JSON.stringify(response.body)}`);
    }

    await this.startContainer(response.body.Id);
    logger.success(`Managed dcrouter container started: ${response.body.Id}`);
  }

  private async writeManagedConfig(dataDirArg: string): Promise<void> {
    const configPath = plugins.path.join(dataDirArg, 'managed-config.json');
    try {
      const existingConfig = await plugins.fs.readTextFile(configPath);
      JSON.parse(existingConfig);
      return;
    } catch (error) {
      if (!plugins.isNotFoundError(error)) {
        throw new Error(`Managed dcrouter config exists but is not valid JSON: ${getErrorMessage(error)}`);
      }
    }

    const config = {
      coreTrafficConfig: {
        routes: [],
      },
    };
    await plugins.fs.writeTextFile(configPath, JSON.stringify(config, null, 2));
  }

  private async getExistingContainer(): Promise<any | null> {
    const filters = encodeURIComponent(JSON.stringify({ name: [containerName] }));
    const response = await this.dockerClient!.request('GET', `/containers/json?all=true&filters=${filters}`, {});
    if (response.statusCode >= 300 || !Array.isArray(response.body)) {
      return null;
    }

    return response.body.find((container: any) => {
      return container.Names?.some((name: string) => name === `/${containerName}` || name === containerName);
    }) ?? null;
  }

  private isContainerRunning(container: any): boolean {
    return container.State === 'running' || Boolean(container.Status?.toLowerCase().startsWith('up '));
  }

  private async startContainer(containerId: string): Promise<void> {
    const response = await this.dockerClient!.request('POST', `/containers/${containerId}/start`, {});
    if (response.statusCode >= 300 && response.statusCode !== 304) {
      throw new Error(`Failed to start managed dcrouter container: HTTP ${response.statusCode}`);
    }
  }

  private async stopContainer(containerId: string): Promise<void> {
    const response = await this.dockerClient!.request('POST', `/containers/${containerId}/stop`, {});
    if (response.statusCode >= 300 && response.statusCode !== 304) {
      throw new Error(`Failed to stop managed dcrouter container: HTTP ${response.statusCode}`);
    }
  }

  private async removeExistingContainer(): Promise<void> {
    const existingContainer = await this.getExistingContainer();
    if (!existingContainer) {
      return;
    }

    const response = await this.dockerClient!.request('DELETE', `/containers/${existingContainer.Id}?force=true`, {});
    if (response.statusCode >= 300) {
      throw new Error(`Failed to remove managed dcrouter container: HTTP ${response.statusCode}`);
    }
  }

  private async checkHealthy(): Promise<boolean> {
    try {
      const response = await fetch(await this.getGatewayUrl());
      return response.ok;
    } catch {
      return false;
    }
  }

  private async waitForReady(maxAttempts = 30, intervalMs = 1000): Promise<void> {
    for (let i = 0; i < maxAttempts; i++) {
      if (await this.checkHealthy()) {
        return;
      }
      await new Promise((resolve) => setTimeout(resolve, intervalMs));
    }

    throw new Error('Managed dcrouter did not become ready in time');
  }
}
