import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import Docker from 'dockerode';

interface ContainerInfo {
  id: string;
  name: string;
  state: string;
  status: string;
  labels: Record<string, string>;
}

interface OrchestrationResult {
  success: boolean;
  message: string;
  containersStarted: string[];
  containersStopped: string[];
  containersRemoved: string[];
  errors: string[];
  estimatedTime: number; // Estimated restart time in seconds
}

@Injectable()
export class DockerService implements OnModuleInit {
  private readonly logger = new Logger(DockerService.name);
  private docker: Docker | null = null;
  private isAvailable = false;

  async onModuleInit() {
    await this.initializeDocker();
    // Bootstrap orchestration: start containers based on saved config
    await this.bootstrapOrchestration();
  }

  /**
   * Bootstrap orchestration: start built-in containers based on saved config
   * This runs on application startup to ensure containers match saved configuration
   */
  private async bootstrapOrchestration(): Promise<void> {
    if (!this.isAvailable) {
      this.logger.log('[Bootstrap Orchestration] Docker not available, skipping');
      return;
    }

    const profiles: string[] = [];

    // Check for built-in services from environment variables
    if (process.env.REDIS_BUILTIN === 'true') {
      profiles.push('redis');
    }
    if (process.env.POSTGRES_BUILTIN === 'true') {
      profiles.push('postgres');
    }
    if (process.env.MINIO_BUILTIN === 'true') {
      profiles.push('minio');
    }

    if (profiles.length === 0) {
      this.logger.log('[Bootstrap Orchestration] No built-in services configured');
      return;
    }

    this.logger.log(`[Bootstrap Orchestration] Starting built-in services: ${profiles.join(', ')}`);
    const result = await this.orchestrateProfiles(profiles);

    if (result.success) {
      this.logger.log(`[Bootstrap Orchestration] Started ${result.containersStarted.length} container(s)`);
    } else {
      this.logger.warn(`[Bootstrap Orchestration] Issues: ${result.errors.join('; ')}`);
    }
  }

  private async initializeDocker(): Promise<void> {
    try {
      this.docker = new Docker({ socketPath: '/var/run/docker.sock' });
      await this.docker.ping();
      this.isAvailable = true;
      this.logger.log('Docker API connected successfully');
    } catch (error) {
      this.logger.warn(
        'Docker socket not available. Container orchestration disabled.',
        error instanceof Error ? error.message : error,
      );
      this.isAvailable = false;
    }
  }

  /**
   * Check if Docker is available
   */
  isDockerAvailable(): boolean {
    return this.isAvailable;
  }

  /**
   * List all OpenWA-related containers
   */
  async listContainers(): Promise<ContainerInfo[]> {
    if (!this.docker || !this.isAvailable) {
      return [];
    }

    try {
      const containers = await this.docker.listContainers({ all: true });
      return containers
        .filter(c => {
          // Filter by OpenWA labels or name prefix
          const labels = c.Labels || {};
          return labels['com.openwa.service'] || c.Names?.some(n => n.startsWith('/openwa-'));
        })
        .map(c => ({
          id: c.Id.substring(0, 12),
          name: c.Names?.[0]?.replace(/^\//, '') || 'unknown',
          state: c.State || 'unknown',
          status: c.Status || 'unknown',
          labels: c.Labels || {},
        }));
    } catch (error) {
      this.logger.error('Failed to list containers', error);
      return [];
    }
  }

  /**
   * Get container by service name or label
   */
  async getContainerByService(service: string): Promise<Docker.Container | null> {
    if (!this.docker || !this.isAvailable) {
      return null;
    }

    try {
      const containers = await this.docker.listContainers({
        all: true,
        filters: {
          label: [`com.openwa.service=${service}`],
        },
      });

      if (containers.length > 0) {
        return this.docker.getContainer(containers[0].Id);
      }

      // Fallback: try by name
      const allContainers = await this.docker.listContainers({ all: true });
      const match = allContainers.find(c => c.Names?.some(n => n.includes(`openwa-${service}`) || n.includes(service)));

      if (match) {
        return this.docker.getContainer(match.Id);
      }

      return null;
    } catch (error) {
      this.logger.error(`Failed to get container for service: ${service}`, error);
      return null;
    }
  }

  /**
   * Container specifications for optional services
   * Mirrors docker-compose.yml settings but uses Docker API directly
   */
  private getContainerSpec(profile: string): {
    image: string;
    name: string;
    alias: string; // DNS alias for network resolution
    env?: string[];
    cmd?: string[];
    volumes?: { name: string; path: string }[];
    healthcheck?: { test: string[]; interval: number; timeout: number; retries: number };
    labels: Record<string, string>;
    ports?: { container: number; host: number }[];
  } | null {
    const specs: Record<string, ReturnType<typeof this.getContainerSpec>> = {
      redis: {
        image: 'redis:7-alpine',
        name: 'openwa-redis',
        alias: 'redis', // DNS alias for resolution
        cmd: ['redis-server', '--appendonly', 'yes'],
        volumes: [{ name: 'openwa_redis-data', path: '/data' }],
        healthcheck: {
          test: ['CMD', 'redis-cli', 'ping'],
          interval: 5000000000, // 5s in nanoseconds
          timeout: 3000000000,
          retries: 5,
        },
        labels: {
          'com.openwa.service': 'cache',
          'com.openwa.builtin': 'true',
        },
      },
      postgres: {
        image: 'postgres:16-alpine',
        name: 'openwa-postgres',
        alias: 'postgres',
        // Use hardcoded defaults for built-in container (don't inherit SQLite paths)
        env: ['POSTGRES_USER=openwa', 'POSTGRES_PASSWORD=openwa', 'POSTGRES_DB=openwa'],
        volumes: [{ name: 'openwa_postgres-data', path: '/var/lib/postgresql/data' }],
        healthcheck: {
          test: ['CMD-SHELL', 'pg_isready -U openwa'],
          interval: 5000000000,
          timeout: 3000000000,
          retries: 5,
        },
        labels: {
          'com.openwa.service': 'database',
          'com.openwa.builtin': 'true',
        },
      },
      minio: {
        image: 'minio/minio',
        name: 'openwa-minio',
        alias: 'minio',
        cmd: ['server', '/data', '--console-address', ':9001'],
        env: [
          `MINIO_ROOT_USER=${process.env.S3_ACCESS_KEY || 'minioadmin'}`,
          `MINIO_ROOT_PASSWORD=${process.env.S3_SECRET_KEY || 'minioadmin'}`,
        ],
        volumes: [{ name: 'openwa_minio-data', path: '/data' }],
        ports: [
          { container: 9000, host: 9000 },
          { container: 9001, host: 9001 },
        ],
        healthcheck: {
          test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live'],
          interval: 10000000000,
          timeout: 5000000000,
          retries: 3,
        },
        labels: {
          'com.openwa.service': 'storage',
          'com.openwa.builtin': 'true',
        },
      },
    };
    return specs[profile] || null;
  }

  /**
   * Create and start a service using Docker API directly
   */
  async createService(profile: string): Promise<boolean> {
    if (!this.docker || !this.isAvailable) {
      this.logger.error('Docker not available for creating service');
      return false;
    }

    const spec = this.getContainerSpec(profile);
    if (!spec) {
      this.logger.error(`Unknown profile: ${profile}`);
      return false;
    }

    this.logger.log(`Creating service: ${profile} (image: ${spec.image})`);

    try {
      // Check if container already exists
      const existing = await this.getContainerByService(profile);
      if (existing) {
        const info = await existing.inspect();
        if (info.State.Running) {
          this.logger.log(`Container ${spec.name} already running`);
          return true;
        }
        // Start existing container
        await existing.start();
        this.logger.log(`Started existing container: ${spec.name}`);
        return true;
      }

      // Pull image first
      this.logger.log(`Pulling image: ${spec.image}`);
      await new Promise<void>((resolve, reject) => {
        void this.docker!.pull(spec.image, (err: Error | null, stream: NodeJS.ReadableStream) => {
          if (err) return reject(err);
          this.docker!.modem.followProgress(stream, (err2: Error | null) => {
            if (err2) return reject(err2);
            resolve();
          });
        });
      });

      // Create volume if needed
      if (spec.volumes) {
        for (const vol of spec.volumes) {
          try {
            await this.docker.createVolume({ Name: vol.name });
            this.logger.log(`Created volume: ${vol.name}`);
          } catch (error) {
            this.logger.debug(`Volume ${vol.name} creation skipped (may already exist)`, { error: String(error) });
          }
        }
      }

      // Create container
      const containerConfig: Docker.ContainerCreateOptions = {
        name: spec.name,
        Image: spec.image,
        Cmd: spec.cmd,
        Env: spec.env,
        Labels: spec.labels,
        HostConfig: {
          NetworkMode: 'openwa-network',
          RestartPolicy: { Name: 'unless-stopped' },
          Binds: spec.volumes?.map(v => `${v.name}:${v.path}`),
          PortBindings: spec.ports?.reduce(
            (acc, p) => {
              acc[`${p.container}/tcp`] = [{ HostIp: '127.0.0.1', HostPort: p.host.toString() }];
              return acc;
            },
            {} as Record<string, { HostIp: string; HostPort: string }[]>,
          ),
        },
        Healthcheck: spec.healthcheck
          ? {
              Test: spec.healthcheck.test,
              Interval: spec.healthcheck.interval,
              Timeout: spec.healthcheck.timeout,
              Retries: spec.healthcheck.retries,
            }
          : undefined,
        NetworkingConfig: {
          EndpointsConfig: {
            'openwa-network': {
              Aliases: [spec.alias, profile], // Add DNS aliases for network resolution
            },
          },
        },
      };

      const container = await this.docker.createContainer(containerConfig);
      await container.start();
      this.logger.log(`Created and started container: ${spec.name}`);
      return true;
    } catch (error) {
      this.logger.error(`Failed to create service ${profile}: ${error instanceof Error ? error.message : error}`);
      return false;
    }
  }

  /**
   * Start a container by service name - creates if not exists
   */
  async startService(service: string): Promise<boolean> {
    const container = await this.getContainerByService(service);

    if (!container) {
      // Container doesn't exist - create it using docker-compose
      this.logger.log(`Container for service '${service}' not found, creating...`);

      // Map service names to docker-compose profiles
      const serviceToProfile: Record<string, string> = {
        database: 'postgres',
        cache: 'redis',
        storage: 'minio',
        postgres: 'postgres',
        redis: 'redis',
        minio: 'minio',
      };

      const profile = serviceToProfile[service] || service;
      return this.createService(profile);
    }

    try {
      const info = await container.inspect();
      if (info.State.Running) {
        this.logger.log(`Service '${service}' is already running`);
        return true;
      }

      await container.start();
      this.logger.log(`Started service: ${service}`);
      return true;
    } catch (error) {
      this.logger.error(`Failed to start service: ${service}`, error);
      return false;
    }
  }

  /**
   * Stop and remove a container by service name to save space
   */
  async removeService(profile: string): Promise<boolean> {
    this.logger.log(`Removing service with profile: ${profile}`);

    // First try to get the container and remove via dockerode
    const serviceMap: Record<string, string> = {
      postgres: 'database',
      redis: 'cache',
      minio: 'storage',
    };

    const service = serviceMap[profile] || profile;
    const container = await this.getContainerByService(service);

    if (container) {
      try {
        const info = await container.inspect();
        if (info.State.Running) {
          await container.stop();
          this.logger.log(`Stopped container: ${profile}`);
        }
        await container.remove({ v: true }); // v: true removes volumes too
        this.logger.log(`Removed container: ${profile}`);
        return true;
      } catch (error) {
        this.logger.error(`Failed to remove container: ${error instanceof Error ? error.message : error}`);
        return false;
      }
    }

    // Container doesn't exist - that's fine for removal
    this.logger.log(`Container for service '${profile}' not found, nothing to remove`);
    return true;
  }

  /**
   * Stop a container by service name (without removing)
   */
  async stopService(service: string): Promise<boolean> {
    const container = await this.getContainerByService(service);
    if (!container) {
      this.logger.warn(`Container for service '${service}' not found`);
      return true; // Already doesn't exist
    }

    try {
      const info = await container.inspect();
      if (!info.State.Running) {
        this.logger.log(`Service '${service}' is already stopped`);
        return true;
      }

      await container.stop();
      this.logger.log(`Stopped service: ${service}`);
      return true;
    } catch (error) {
      this.logger.error(`Failed to stop service: ${service}`, error);
      return false;
    }
  }

  /**
   * Orchestrate services based on required profiles
   * This will start containers that match the profiles
   */
  async orchestrateProfiles(profiles: string[]): Promise<OrchestrationResult> {
    // Calculate estimated time based on profiles
    // Base: 15 seconds for core restart (increased for reliability)
    let estimatedTime = 15;
    if (profiles.includes('postgres')) estimatedTime += 20; // PostgreSQL takes longer
    if (profiles.includes('redis')) estimatedTime += 13;
    if (profiles.includes('minio')) estimatedTime += 15;

    const result: OrchestrationResult = {
      success: true,
      message: '',
      containersStarted: [],
      containersStopped: [],
      containersRemoved: [],
      errors: [],
      estimatedTime,
    };

    if (!this.docker || !this.isAvailable) {
      result.success = false;
      result.message = 'Docker is not available';
      return result;
    }

    this.logger.log(`Orchestrating profiles: ${profiles.join(', ')}`);

    // Map profiles to service names
    const profileToService: Record<string, string> = {
      postgres: 'database',
      redis: 'cache',
      minio: 'storage',
    };

    for (const profile of profiles) {
      const service = profileToService[profile] || profile;
      try {
        const started = await this.startService(service);
        if (started) {
          result.containersStarted.push(profile);
        } else {
          // Container might not exist yet - this is expected for first-time setup
          result.errors.push(
            `Service '${profile}' container not found. It may need to be created first with docker-compose.`,
          );
        }
      } catch (error) {
        result.errors.push(`Failed to start ${profile}: ${error instanceof Error ? error.message : 'Unknown error'}`);
      }
    }

    if (result.errors.length > 0) {
      result.success = profiles.length > 0 && result.containersStarted.length > 0;
      result.message = result.errors.join('; ');
    } else {
      result.message = `Successfully orchestrated ${result.containersStarted.length} service(s)`;
    }

    return result;
  }

  /**
   * Get Docker system info
   */
  async getSystemInfo(): Promise<{ available: boolean; info?: Record<string, unknown> }> {
    if (!this.docker || !this.isAvailable) {
      return { available: false };
    }

    try {
      const info = (await this.docker.info()) as {
        Containers: number;
        ContainersRunning: number;
        ContainersPaused: number;
        ContainersStopped: number;
        Images: number;
        ServerVersion: string;
        OperatingSystem: string;
        Architecture: string;
      };
      return {
        available: true,
        info: {
          containers: info.Containers,
          containersRunning: info.ContainersRunning,
          containersPaused: info.ContainersPaused,
          containersStopped: info.ContainersStopped,
          images: info.Images,
          serverVersion: info.ServerVersion,
          operatingSystem: info.OperatingSystem,
          architecture: info.Architecture,
        },
      };
    } catch (error) {
      this.logger.error('Failed to get Docker info', error);
      return { available: false };
    }
  }
}
