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 { DockerImage } from './classes.image.js';
import { DockerSecret } from './classes.secret.js';
import { logger } from './logger.js';

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

  /**
   * Internal: Get all services
   * Public API: Use dockerHost.listServices() instead
   */
  public static async _list(dockerHost: DockerHost) {
    const services: DockerService[] = [];
    const response = await dockerHost.request('GET', '/services');
    for (const serviceObject of response.body) {
      const dockerService = new DockerService(dockerHost);
      Object.assign(dockerService, serviceObject);
      services.push(dockerService);
    }
    return services;
  }

  /**
   * Internal: Get service by name
   * Public API: Use dockerHost.getServiceByName(name) instead
   */
  public static async _fromName(
    dockerHost: DockerHost,
    networkName: string,
  ): Promise<DockerService> {
    const allServices = await DockerService._list(dockerHost);
    const wantedService = allServices.find((service) => {
      return service.Spec.Name === networkName;
    });
    if (!wantedService) {
      throw new Error(`Service not found: ${networkName}`);
    }
    return wantedService;
  }

  /**
   * Internal: Create a service
   * Public API: Use dockerHost.createService(descriptor) instead
   */
  public static async _create(
    dockerHost: DockerHost,
    serviceCreationDescriptor: interfaces.IServiceCreationDescriptor,
  ): Promise<DockerService> {
    logger.log(
      'info',
      `now creating service ${serviceCreationDescriptor.name}`,
    );

    // Resolve image (support both string and DockerImage instance)
    let imageInstance: DockerImage;
    if (typeof serviceCreationDescriptor.image === 'string') {
      const foundImage = await DockerImage._fromName(dockerHost, serviceCreationDescriptor.image);
      if (!foundImage) {
        throw new Error(`Image not found: ${serviceCreationDescriptor.image}`);
      }
      imageInstance = foundImage;
    } else {
      imageInstance = serviceCreationDescriptor.image;
    }

    const serviceVersion = await imageInstance.getVersion();

    const labels: interfaces.TLabels = {
      ...serviceCreationDescriptor.labels,
      version: serviceVersion,
    };

    const mounts: Array<{
      /**
       * the target inside the container
       */
      Target: string;
      /**
       * The Source from which to mount the data (Volume or host path)
       */
      Source: string;
      Type: 'bind' | 'volume' | 'tmpfs' | 'npipe';
      ReadOnly: boolean;
      Consistency: 'default' | 'consistent' | 'cached' | 'delegated';
    }> = [];
    if (serviceCreationDescriptor.accessHostDockerSock) {
      mounts.push({
        Target: '/var/run/docker.sock',
        Source: '/var/run/docker.sock',
        Consistency: 'default',
        ReadOnly: false,
        Type: 'bind',
      });
    }

    if (
      serviceCreationDescriptor.resources &&
      serviceCreationDescriptor.resources.volumeMounts
    ) {
      for (const volumeMount of serviceCreationDescriptor.resources
        .volumeMounts) {
        mounts.push({
          Target: volumeMount.containerFsPath,
          Source: volumeMount.hostFsPath,
          Consistency: 'default',
          ReadOnly: false,
          Type: 'bind',
        });
      }
    }

    // Resolve networks (support both string[] and DockerNetwork[])
    const networkArray: Array<{
      Target: string;
      Aliases: string[];
    }> = [];

    for (const network of serviceCreationDescriptor.networks) {
      // Skip null networks (can happen if network creation fails)
      if (!network) {
        logger.log('warn', 'Skipping null network in service creation');
        continue;
      }

      // Resolve network name
      const networkName = typeof network === 'string' ? network : network.Name;
      networkArray.push({
        Target: networkName,
        Aliases: [serviceCreationDescriptor.networkAlias],
      });
    }

    const ports: Array<{ Protocol: string; PublishedPort: number; TargetPort: number }> = [];
    for (const port of serviceCreationDescriptor.ports) {
      const portArray = port.split(':');
      const hostPort = portArray[0];
      const containerPort = portArray[1];
      ports.push({
        Protocol: 'tcp',
        PublishedPort: parseInt(hostPort, 10),
        TargetPort: parseInt(containerPort, 10),
      });
    }

    // Resolve secrets (support both string[] and DockerSecret[])
    const secretArray: any[] = [];
    for (const secret of serviceCreationDescriptor.secrets) {
      // Resolve secret instance
      let secretInstance: DockerSecret;
      if (typeof secret === 'string') {
        const foundSecret = await DockerSecret._fromName(dockerHost, secret);
        if (!foundSecret) {
          throw new Error(`Secret not found: ${secret}`);
        }
        secretInstance = foundSecret;
      } else {
        secretInstance = secret;
      }

      secretArray.push({
        File: {
          Name: 'secret.json', // TODO: make sure that works with multiple secrets
          UID: '33',
          GID: '33',
          Mode: 384,
        },
        SecretID: secretInstance.ID,
        SecretName: secretInstance.Spec.Name,
      });
    }

    // lets configure limits

    const memoryLimitMB = serviceCreationDescriptor.resources?.memorySizeMB ?? 1000;

    const limits = {
      MemoryBytes: memoryLimitMB * 1000000,
    };

    const response = await dockerHost.request('POST', '/services/create', {
      Name: serviceCreationDescriptor.name,
      TaskTemplate: {
        ContainerSpec: {
          Image: imageInstance.RepoTags[0],
          Labels: labels,
          Secrets: secretArray,
          Mounts: mounts,
          /* DNSConfig: {
            Nameservers: ['1.1.1.1']
          } */
        },
        UpdateConfig: {
          Parallelism: 0,
          Delay: 0,
          FailureAction: 'pause',
          Monitor: 15000000000,
          MaxFailureRatio: 0.15,
        },
        ForceUpdate: 1,
        Resources: {
          Limits: limits,
        },
        Networks: networkArray,
        LogDriver: {
          Name: 'json-file',
          Options: {
            'max-file': '3',
            'max-size': '10M',
          },
        },
      },
      Labels: labels,
      EndpointSpec: {
        Ports: ports,
      },
    });

    const createdService = await DockerService._fromName(
      dockerHost,
      serviceCreationDescriptor.name,
    );
    return createdService;
  }

  // INSTANCE PROPERTIES
  // Note: dockerHost (not dockerHostRef) for consistency with base class

  public ID!: string;
  public Version!: { Index: number };
  public CreatedAt!: string;
  public UpdatedAt!: string;
  public Spec!: {
    Name: string;
    Labels: interfaces.TLabels;
    TaskTemplate: {
      ContainerSpec: {
        Image: string;
        Isolation: string;
        Secrets: Array<{
          File: {
            Name: string;
            UID: string;
            GID: string;
            Mode: number;
          };
          SecretID: string;
          SecretName: string;
        }>;
      };
      ForceUpdate: 0;
      Networks: Array<{
        Target: string;
        Aliases: string[];
      }>;
    };
    Mode: {};
  };
  public Endpoint!: { Spec: {}; VirtualIPs: [any[]] };

  constructor(dockerHostArg: DockerHost) {
    super(dockerHostArg);
  }

  // INSTANCE METHODS

  /**
   * Refreshes this service's state from the Docker daemon
   */
  public async refresh(): Promise<void> {
    const updated = await DockerService._fromName(this.dockerHost, this.Spec.Name);
    if (updated) {
      Object.assign(this, updated);
    }
  }

  /**
   * Removes this service from the Docker daemon
   */
  public async remove() {
    await this.dockerHost.request('DELETE', `/services/${this.ID}`);
  }

  /**
   * Re-reads service data from Docker engine
   * @deprecated Use refresh() instead
   */
  public async reReadFromDockerEngine() {
    const dockerData = await this.dockerHost.request(
      'GET',
      `/services/${this.ID}`,
    );
    // TODO: Better assign: Object.assign(this, dockerData);
  }

  /**
   * Checks if this service needs an update based on image version
   */
  public async needsUpdate(): Promise<boolean> {
    // TODO: implement digest based update recognition

    await this.reReadFromDockerEngine();
    const dockerImage = await DockerImage._createFromRegistry(
      this.dockerHost,
      {
        creationObject: {
          imageUrl: this.Spec.TaskTemplate.ContainerSpec.Image,
        },
      },
    );

    const imageVersion = new plugins.smartversion.SmartVersion(
      dockerImage.Labels.version,
    );
    const serviceVersion = new plugins.smartversion.SmartVersion(
      this.Spec.Labels.version,
    );
    if (imageVersion.greaterThan(serviceVersion)) {
      console.log(`service ${this.Spec.Name}  needs to be updated`);
      return true;
    } else {
      console.log(`service ${this.Spec.Name} is up to date.`);
      return false;
    }
  }
}
