import * as plugins from './plugins.js';
import * as interfaces from './interfaces/index.js';

import { DockerHost } from './classes.host.js';
import { DockerResource } from './classes.base.js';
import { logger } from './logger.js';

export class DockerContainer extends DockerResource {
  // STATIC (Internal - prefixed with _ to indicate internal use)

  /**
   * Internal: Get all containers
   * Public API: Use dockerHost.listContainers() instead
   */
  public static async _list(
    dockerHostArg: DockerHost,
  ): Promise<DockerContainer[]> {
    const result: DockerContainer[] = [];
    const response = await dockerHostArg.request('GET', '/containers/json');

    // TODO: Think about getting the config by inspecting the container
    for (const containerResult of response.body) {
      result.push(new DockerContainer(dockerHostArg, containerResult));
    }
    return result;
  }

  /**
   * Internal: Get a container by ID
   * Public API: Use dockerHost.getContainerById(id) instead
   * Returns undefined if container does not exist
   */
  public static async _fromId(
    dockerHostArg: DockerHost,
    containerId: string,
  ): Promise<DockerContainer | undefined> {
    const containers = await this._list(dockerHostArg);
    return containers.find((container) => container.Id === containerId);
  }

  /**
   * Internal: Create a container
   * Public API: Use dockerHost.createContainer(descriptor) instead
   */
  public static async _create(
    dockerHost: DockerHost,
    containerCreationDescriptor: interfaces.IContainerCreationDescriptor,
  ): Promise<DockerContainer> {
    // Check for unique hostname
    const existingContainers = await DockerContainer._list(dockerHost);
    const sameHostNameContainer = existingContainers.find((container) => {
      // TODO implement HostName Detection;
      return false;
    });

    const response = await dockerHost.request('POST', '/containers/create', {
      Hostname: containerCreationDescriptor.Hostname,
      Domainname: containerCreationDescriptor.Domainname,
      User: 'root',
    });

    if (response.statusCode < 300) {
      logger.log('info', 'Container created successfully');
      // Return the created container instance
      const container = await DockerContainer._fromId(dockerHost, response.body.Id);
      if (!container) {
        throw new Error('Container was created but could not be retrieved');
      }
      return container;
    } else {
      logger.log('error', 'There has been a problem when creating the container');
      throw new Error(`Failed to create container: ${response.statusCode}`);
    }
  }

  // INSTANCE PROPERTIES
  public Id!: string;
  public Names!: string[];
  public Image!: string;
  public ImageID!: string;
  public Command!: string;
  public Created!: number;
  public Ports!: interfaces.TPorts;
  public Labels!: interfaces.TLabels;
  public State!: string;
  public Status!: string;
  public HostConfig: any;
  public NetworkSettings!: {
    Networks: {
      [key: string]: {
        IPAMConfig: any;
        Links: any;
        Aliases: any;
        NetworkID: string;
        EndpointID: string;
        Gateway: string;
        IPAddress: string;
        IPPrefixLen: number;
        IPv6Gateway: string;
        GlobalIPv6Address: string;
        GlobalIPv6PrefixLen: number;
        MacAddress: string;
        DriverOpts: any;
      };
    };
  };
  public Mounts: any;

  constructor(dockerHostArg: DockerHost, dockerContainerObjectArg: any) {
    super(dockerHostArg);
    Object.keys(dockerContainerObjectArg).forEach((keyArg) => {
      this[keyArg] = dockerContainerObjectArg[keyArg];
    });
  }

  // INSTANCE METHODS

  /**
   * Refreshes this container's state from the Docker daemon
   */
  public async refresh(): Promise<void> {
    const updated = await DockerContainer._fromId(this.dockerHost, this.Id);
    Object.assign(this, updated);
  }

  /**
   * Inspects the container and returns detailed information
   */
  public async inspect(): Promise<any> {
    const response = await this.dockerHost.request('GET', `/containers/${this.Id}/json`);
    // Update instance with fresh data
    Object.assign(this, response.body);
    return response.body;
  }

  /**
   * Starts the container
   */
  public async start(): Promise<void> {
    const response = await this.dockerHost.request('POST', `/containers/${this.Id}/start`);
    if (response.statusCode >= 300) {
      throw new Error(`Failed to start container: ${response.statusCode}`);
    }
    await this.refresh();
  }

  /**
   * Stops the container
   * @param options Options for stopping (e.g., timeout in seconds)
   */
  public async stop(options?: { t?: number }): Promise<void> {
    const queryParams = options?.t ? `?t=${options.t}` : '';
    const response = await this.dockerHost.request('POST', `/containers/${this.Id}/stop${queryParams}`);
    if (response.statusCode >= 300) {
      throw new Error(`Failed to stop container: ${response.statusCode}`);
    }
    await this.refresh();
  }

  /**
   * Removes the container
   * @param options Options for removal (force, remove volumes, remove link)
   */
  public async remove(options?: { force?: boolean; v?: boolean; link?: boolean }): Promise<void> {
    const queryParams = new URLSearchParams();
    if (options?.force) queryParams.append('force', '1');
    if (options?.v) queryParams.append('v', '1');
    if (options?.link) queryParams.append('link', '1');

    const queryString = queryParams.toString();
    const response = await this.dockerHost.request(
      'DELETE',
      `/containers/${this.Id}${queryString ? '?' + queryString : ''}`,
    );

    if (response.statusCode >= 300) {
      throw new Error(`Failed to remove container: ${response.statusCode}`);
    }
  }

  /**
   * Gets container logs
   * @param options Log options (stdout, stderr, timestamps, tail, since, follow)
   */
  public async logs(options?: {
    stdout?: boolean;
    stderr?: boolean;
    timestamps?: boolean;
    tail?: number | 'all';
    since?: number;
    follow?: boolean;
  }): Promise<string> {
    const queryParams = new URLSearchParams();
    queryParams.append('stdout', options?.stdout !== false ? '1' : '0');
    queryParams.append('stderr', options?.stderr !== false ? '1' : '0');
    if (options?.timestamps) queryParams.append('timestamps', '1');
    if (options?.tail) queryParams.append('tail', options.tail.toString());
    if (options?.since) queryParams.append('since', options.since.toString());
    if (options?.follow) queryParams.append('follow', '1');

    const response = await this.dockerHost.request('GET', `/containers/${this.Id}/logs?${queryParams.toString()}`);

    // Docker returns logs with a special format (8 bytes header + payload)
    // For simplicity, we'll return the raw body as string
    return response.body.toString();
  }

  /**
   * Gets container stats
   * @param options Stats options (stream for continuous stats)
   */
  public async stats(options?: { stream?: boolean }): Promise<any> {
    const queryParams = new URLSearchParams();
    queryParams.append('stream', options?.stream ? '1' : '0');

    const response = await this.dockerHost.request('GET', `/containers/${this.Id}/stats?${queryParams.toString()}`);
    return response.body;
  }

  /**
   * Streams container logs continuously (follow mode)
   * Returns a readable stream that emits log data as it's produced
   * @param options Log streaming options
   */
  public async streamLogs(options?: {
    stdout?: boolean;
    stderr?: boolean;
    timestamps?: boolean;
    tail?: number | 'all';
    since?: number;
  }): Promise<plugins.smartstream.stream.Readable> {
    const queryParams = new URLSearchParams();
    queryParams.append('stdout', options?.stdout !== false ? '1' : '0');
    queryParams.append('stderr', options?.stderr !== false ? '1' : '0');
    queryParams.append('follow', '1'); // Always follow for streaming
    if (options?.timestamps) queryParams.append('timestamps', '1');
    if (options?.tail) queryParams.append('tail', options.tail.toString());
    if (options?.since) queryParams.append('since', options.since.toString());

    const response = await this.dockerHost.requestStreaming(
      'GET',
      `/containers/${this.Id}/logs?${queryParams.toString()}`
    );

    // requestStreaming returns Node.js stream
    return response as plugins.smartstream.stream.Readable;
  }

  /**
   * Attaches to the container's main process (PID 1)
   * Returns a duplex stream for bidirectional communication
   * @param options Attach options
   */
  public async attach(options?: {
    stream?: boolean;
    stdin?: boolean;
    stdout?: boolean;
    stderr?: boolean;
    logs?: boolean;
  }): Promise<{
    stream: plugins.smartstream.stream.Duplex;
    close: () => Promise<void>;
  }> {
    const queryParams = new URLSearchParams();
    queryParams.append('stream', options?.stream !== false ? '1' : '0');
    queryParams.append('stdin', options?.stdin ? '1' : '0');
    queryParams.append('stdout', options?.stdout !== false ? '1' : '0');
    queryParams.append('stderr', options?.stderr !== false ? '1' : '0');
    if (options?.logs) queryParams.append('logs', '1');

    const response = await this.dockerHost.requestHijackedStreaming(
      'POST',
      `/containers/${this.Id}/attach?${queryParams.toString()}`,
      {},
    );

    return {
      stream: response.stream,
      close: response.close,
    };
  }

  /**
   * Executes a command in the container
   * Returns a duplex stream for command interaction
   * @param command Command to execute (string or array of strings)
   * @param options Exec options
   */
  public async exec(
    command: string | string[],
    options?: {
      tty?: boolean;
      attachStdin?: boolean;
      attachStdout?: boolean;
      attachStderr?: boolean;
      env?: string[];
      workingDir?: string;
      user?: string;
    }
  ): Promise<{
    stream: plugins.smartstream.stream.Duplex;
    close: () => Promise<void>;
    inspect: () => Promise<interfaces.IExecInspectInfo>;
  }> {
    // Step 1: Create exec instance
    const createResponse = await this.dockerHost.request('POST', `/containers/${this.Id}/exec`, {
      Cmd: typeof command === 'string' ? ['/bin/sh', '-c', command] : command,
      AttachStdin: options?.attachStdin !== false,
      AttachStdout: options?.attachStdout !== false,
      AttachStderr: options?.attachStderr !== false,
      Tty: options?.tty || false,
      Env: options?.env || [],
      WorkingDir: options?.workingDir,
      User: options?.user,
    });

    const execId = createResponse.body.Id;

    // Step 2: Start exec instance with streaming response
    const startResponse = await this.dockerHost.requestHijackedStreaming(
      'POST',
      `/exec/${execId}/start`,
      {
        Detach: false,
        Tty: options?.tty || false,
      }
    );

    const inspect = async (): Promise<interfaces.IExecInspectInfo> => {
      const inspectResponse = await this.dockerHost.request('GET', `/exec/${execId}/json`);
      return inspectResponse.body;
    };

    return {
      stream: startResponse.stream,
      close: startResponse.close,
      inspect,
    };
  }
}
