import * as k8s from '@kubernetes/client-node';
import type { FastifyBaseLogger } from 'fastify';
import * as yaml from 'js-yaml';
import * as fs from 'fs/promises';
import * as path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export interface K8sClientConfig {
  namespace: string;
  inCluster: boolean;
  context?: string; // For local dev
  logger: FastifyBaseLogger;
}

export interface DeploymentSpec {
  userId: string;
  licenseType: 'free' | 'pro' | 'enterprise';
  agentImage: string;
  sidecarImage: string;
  storageClass: string;
}

/**
 * Kubernetes client wrapper for managing agent deployments
 */
export class KubernetesClient {
  private config: K8sClientConfig;
  private k8sConfig: k8s.KubeConfig;
  private appsApi: k8s.AppsV1Api;
  private coreApi: k8s.CoreV1Api;

  constructor(config: K8sClientConfig) {
    this.config = config;
    this.k8sConfig = new k8s.KubeConfig();

    if (config.inCluster) {
      this.k8sConfig.loadFromCluster();
      this.config.logger.info('Loaded in-cluster Kubernetes config');
    } else {
      this.k8sConfig.loadFromDefault();
      if (config.context) {
        this.k8sConfig.setCurrentContext(config.context);
        this.config.logger.info({ context: config.context }, 'Set Kubernetes context');
      }
      this.config.logger.info('Loaded Kubernetes config from default location');
    }

    this.appsApi = this.k8sConfig.makeApiClient(k8s.AppsV1Api);
    this.coreApi = this.k8sConfig.makeApiClient(k8s.CoreV1Api);
  }

  /**
   * Generate deployment name from user ID
   */
  static getDeploymentName(userId: string): string {
    // Sanitize userId to be k8s-compliant (lowercase alphanumeric + hyphens)
    const sanitized = userId.toLowerCase().replace(/[^a-z0-9-]/g, '-');
    return `agent-${sanitized}`;
  }

  /**
   * Generate service name (same as deployment)
   */
  static getServiceName(userId: string): string {
    return this.getDeploymentName(userId);
  }

  /**
   * Generate PVC name
   */
  static getPvcName(userId: string): string {
    return `${this.getDeploymentName(userId)}-data`;
  }

  /**
   * Compute MCP endpoint URL from service name
   */
  static getMcpEndpoint(userId: string, namespace: string): string {
    const serviceName = this.getServiceName(userId);
    return `http://${serviceName}.${namespace}.svc.cluster.local:3000`;
  }

  /**
   * Check if deployment exists
   */
  async deploymentExists(deploymentName: string): Promise<boolean> {
    try {
      await this.appsApi.readNamespacedDeployment(deploymentName, this.config.namespace);
      return true;
    } catch (error: any) {
      if (error.response?.statusCode === 404) {
        return false;
      }
      throw error;
    }
  }

  /**
   * Create agent deployment from template
   */
  async createAgentDeployment(spec: DeploymentSpec): Promise<void> {
    const deploymentName = KubernetesClient.getDeploymentName(spec.userId);
    const serviceName = KubernetesClient.getServiceName(spec.userId);
    const pvcName = KubernetesClient.getPvcName(spec.userId);

    this.config.logger.info(
      { userId: spec.userId, licenseType: spec.licenseType, deploymentName },
      'Creating agent deployment'
    );

    // Load template based on license type
    const templatePath = path.join(
      __dirname,
      'templates',
      `${spec.licenseType}-tier.yaml`
    );

    const templateContent = await fs.readFile(templatePath, 'utf-8');

    // Substitute variables
    const rendered = templateContent
      .replace(/\{\{userId\}\}/g, spec.userId)
      .replace(/\{\{deploymentName\}\}/g, deploymentName)
      .replace(/\{\{serviceName\}\}/g, serviceName)
      .replace(/\{\{pvcName\}\}/g, pvcName)
      .replace(/\{\{agentImage\}\}/g, spec.agentImage)
      .replace(/\{\{sidecarImage\}\}/g, spec.sidecarImage)
      .replace(/\{\{storageClass\}\}/g, spec.storageClass);

    // Parse YAML documents (deployment, pvc, service)
    const documents = yaml.loadAll(rendered) as any[];

    // Apply each resource
    for (const doc of documents) {
      if (!doc || !doc.kind) continue;

      try {
        switch (doc.kind) {
          case 'Deployment':
            await this.appsApi.createNamespacedDeployment(this.config.namespace, doc);
            this.config.logger.info({ deploymentName }, 'Created deployment');
            break;

          case 'PersistentVolumeClaim':
            await this.coreApi.createNamespacedPersistentVolumeClaim(
              this.config.namespace,
              doc
            );
            this.config.logger.info({ pvcName }, 'Created PVC');
            break;

          case 'Service':
            await this.coreApi.createNamespacedService(this.config.namespace, doc);
            this.config.logger.info({ serviceName }, 'Created service');
            break;

          default:
            this.config.logger.warn({ kind: doc.kind }, 'Unknown resource kind in template');
        }
      } catch (error: any) {
        // If resource already exists, log warning but continue
        if (error.response?.statusCode === 409) {
          this.config.logger.warn(
            { kind: doc.kind, name: doc.metadata?.name },
            'Resource already exists, skipping'
          );
        } else {
          throw error;
        }
      }
    }

    this.config.logger.info({ deploymentName }, 'Agent deployment created successfully');
  }

  /**
   * Wait for deployment to be ready
   */
  async waitForDeploymentReady(
    deploymentName: string,
    timeoutMs: number = 120000
  ): Promise<boolean> {
    const startTime = Date.now();
    const pollInterval = 2000; // 2 seconds

    this.config.logger.info(
      { deploymentName, timeoutMs },
      'Waiting for deployment to be ready'
    );

    while (Date.now() - startTime < timeoutMs) {
      try {
        const response = await this.appsApi.readNamespacedDeployment(
          deploymentName,
          this.config.namespace
        );

        const deployment = response.body;
        const status = deployment.status;

        // Check if deployment is ready
        if (
          status?.availableReplicas &&
          status.availableReplicas > 0 &&
          status.readyReplicas &&
          status.readyReplicas > 0
        ) {
          this.config.logger.info({ deploymentName }, 'Deployment is ready');
          return true;
        }

        // Check for failure conditions
        if (status?.conditions) {
          const failedCondition = status.conditions.find(
            (c) => c.type === 'Progressing' && c.status === 'False'
          );
          if (failedCondition) {
            this.config.logger.error(
              { deploymentName, reason: failedCondition.reason, message: failedCondition.message },
              'Deployment failed to progress'
            );
            return false;
          }
        }

        this.config.logger.debug(
          {
            deploymentName,
            replicas: status?.replicas,
            ready: status?.readyReplicas,
            available: status?.availableReplicas,
          },
          'Deployment not ready yet, waiting...'
        );

        await new Promise((resolve) => setTimeout(resolve, pollInterval));
      } catch (error: any) {
        if (error.response?.statusCode === 404) {
          this.config.logger.warn({ deploymentName }, 'Deployment not found');
          return false;
        }
        throw error;
      }
    }

    this.config.logger.warn({ deploymentName, timeoutMs }, 'Deployment readiness timeout');
    return false;
  }

  /**
   * Get service endpoint URL
   */
  async getServiceEndpoint(serviceName: string): Promise<string | null> {
    try {
      const response = await this.coreApi.readNamespacedService(
        serviceName,
        this.config.namespace
      );

      const service = response.body;

      // For ClusterIP services, return internal DNS name
      if (service.spec?.type === 'ClusterIP') {
        const port = service.spec.ports?.find((p) => p.name === 'mcp')?.port || 3000;
        return `http://${serviceName}.${this.config.namespace}.svc.cluster.local:${port}`;
      }

      // For other service types (NodePort, LoadBalancer), would need different logic
      this.config.logger.warn(
        { serviceName, type: service.spec?.type },
        'Unexpected service type'
      );
      return null;
    } catch (error: any) {
      if (error.response?.statusCode === 404) {
        this.config.logger.warn({ serviceName }, 'Service not found');
        return null;
      }
      throw error;
    }
  }

  /**
   * Delete deployment and associated resources
   * (Used for cleanup/testing - normally handled by lifecycle sidecar)
   */
  async deleteAgentDeployment(userId: string): Promise<void> {
    const deploymentName = KubernetesClient.getDeploymentName(userId);
    const serviceName = KubernetesClient.getServiceName(userId);
    const pvcName = KubernetesClient.getPvcName(userId);

    this.config.logger.info({ userId, deploymentName }, 'Deleting agent deployment');

    // Delete deployment
    try {
      await this.appsApi.deleteNamespacedDeployment(deploymentName, this.config.namespace);
      this.config.logger.info({ deploymentName }, 'Deleted deployment');
    } catch (error: any) {
      if (error.response?.statusCode !== 404) {
        this.config.logger.warn({ deploymentName, error }, 'Failed to delete deployment');
      }
    }

    // Delete service
    try {
      await this.coreApi.deleteNamespacedService(serviceName, this.config.namespace);
      this.config.logger.info({ serviceName }, 'Deleted service');
    } catch (error: any) {
      if (error.response?.statusCode !== 404) {
        this.config.logger.warn({ serviceName, error }, 'Failed to delete service');
      }
    }

    // Delete PVC
    try {
      await this.coreApi.deleteNamespacedPersistentVolumeClaim(pvcName, this.config.namespace);
      this.config.logger.info({ pvcName }, 'Deleted PVC');
    } catch (error: any) {
      if (error.response?.statusCode !== 404) {
        this.config.logger.warn({ pvcName, error }, 'Failed to delete PVC');
      }
    }
  }
}
