/**
 * Deployment Service
 * Orchestrates deployments using Terraform, Helm, Ansible, and Kubernetes
 */

import { getDb } from '../db/index.js'
import { logger } from '../lib/logger.js'
import { Context } from '../types/context.js'
import { templateService, Template } from './template.js'
import { terraformRenderer } from '../lib/terraform-renderer.js'
import { catalogService } from './catalog.js'
import { terraformExecutor } from '../lib/terraform-executor.js'
import { ansibleExecutor } from '../lib/ansible-executor.js'
import { helmExecutor } from '../lib/helm-executor.js'
import { k8sOrchestrator } from './k8s-orchestrator.js'
import { promises as fs } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'

export enum DeploymentStatus {
  PENDING = 'PENDING',
  PROVISIONING = 'PROVISIONING',
  DEPLOYING = 'DEPLOYING',
  RUNNING = 'RUNNING',
  UPDATING = 'UPDATING',
  STOPPED = 'STOPPED',
  FAILED = 'FAILED',
  DELETING = 'DELETING',
  DELETED = 'DELETED',
}

export enum DeploymentType {
  TERRAFORM = 'TERRAFORM',
  HELM = 'HELM',
  ANSIBLE = 'ANSIBLE',
  KUBERNETES = 'KUBERNETES',
  HYBRID = 'HYBRID',
}

export interface Deployment {
  id: string
  name: string
  productId?: string
  productVersionId?: string
  templateId?: string
  templateVersionId?: string
  tenantId: string
  region?: string
  status: DeploymentStatus
  deploymentType: DeploymentType
  parameters: Record<string, any>
  renderedContent?: string
  terraformState?: Record<string, any>
  outputs: Record<string, any>
  errorMessage?: string
  createdBy?: string
  startedAt?: Date
  completedAt?: Date
  createdAt: Date
  updatedAt: Date
}

export interface DeploymentLog {
  id: string
  deploymentId: string
  level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR'
  message: string
  metadata: Record<string, any>
  createdAt: Date
}

export interface DeploymentEvent {
  id: string
  deploymentId: string
  eventType: string
  eventData: Record<string, any>
  createdBy?: string
  createdAt: Date
}

export interface CreateDeploymentInput {
  name: string
  productId?: string
  productVersionId?: string
  templateId?: string
  templateVersionId?: string
  region?: string
  deploymentType: DeploymentType
  parameters: Record<string, any>
  tags?: Record<string, string>
}

class DeploymentService {
  /**
   * Create deployment
   */
  async createDeployment(
    context: Context,
    input: CreateDeploymentInput
  ): Promise<Deployment> {
    if (!context.user) {
      throw new Error('Authentication required')
    }

    const db = getDb()

    // Get tenant from context
    const tenantId = context.tenant?.id
    if (!tenantId) {
      throw new Error('Tenant context required')
    }

    // Load template if provided
    let template: Template | null = null
    if (input.templateId) {
      template = await templateService.getTemplate(context, input.templateId)
      if (!template) {
        throw new Error(`Template not found: ${input.templateId}`)
      }
    } else if (input.productId) {
      // Load template from product version
      const product = await catalogService.getProduct(context, input.productId)
      if (!product) {
        throw new Error(`Product not found: ${input.productId}`)
      }

      const version = input.productVersionId
        ? product.versions?.find((v) => v.id === input.productVersionId)
        : product.versions?.find((v) => v.isLatest)

      if (version?.templateId) {
        template = await templateService.getTemplate(context, version.templateId)
      }
    }

    if (!template) {
      throw new Error('Template or product with template required')
    }

    // Render template
    let renderedContent: string | undefined
    if (template.templateType === 'PTF' || template.templateType === 'TERRAFORM') {
      renderedContent = await terraformRenderer.render(template, {
        parameters: input.parameters,
        region: input.region,
        tags: input.tags,
      })
    }

    // Create deployment record
    const result = await db.query(
      `INSERT INTO deployments (
        name, product_id, product_version_id, template_id, template_version_id,
        tenant_id, region, deployment_type, parameters, rendered_content,
        status, created_by, started_at
      ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
      RETURNING *`,
      [
        input.name,
        input.productId || null,
        input.productVersionId || null,
        input.templateId || null,
        input.templateVersionId || null,
        tenantId,
        input.region || null,
        input.deploymentType,
        JSON.stringify(input.parameters),
        renderedContent || null,
        DeploymentStatus.PENDING,
        context.user.id,
        new Date(),
      ]
    )

    const deployment = this.mapDeployment(result.rows[0])

    // Log deployment creation
    await this.logDeployment(deployment.id, 'INFO', 'Deployment created', {
      deploymentType: input.deploymentType,
      templateId: template.id,
    })

    // Record event
    await this.recordEvent(deployment.id, 'DEPLOYMENT_CREATED', {
      deploymentType: input.deploymentType,
      templateId: template.id,
    }, context.user.id)

    // Start deployment process (async)
    this.startDeployment(deployment.id, template, input).catch((error) => {
      logger.error('Failed to start deployment', {
        deploymentId: deployment.id,
        error: error instanceof Error ? error.message : String(error),
      })
    })

    logger.info('Deployment created', { deploymentId: deployment.id })
    return deployment
  }

  /**
   * Start deployment process
   */
  private async startDeployment(
    deploymentId: string,
    template: Template,
    input: CreateDeploymentInput
  ): Promise<void> {
    const db = getDb()

    try {
      // Update status to PROVISIONING
      await db.query(
        `UPDATE deployments SET status = $1 WHERE id = $2`,
        [DeploymentStatus.PROVISIONING, deploymentId]
      )

      await this.logDeployment(deploymentId, 'INFO', 'Starting deployment', {
        templateType: template.templateType,
      })

      // Execute deployment based on type
      switch (input.deploymentType) {
        case DeploymentType.TERRAFORM:
          await this.deployWithTerraform(deploymentId, template, input)
          break
        case DeploymentType.HELM:
          await this.deployWithHelm(deploymentId, template, input)
          break
        case DeploymentType.ANSIBLE:
          await this.deployWithAnsible(deploymentId, template, input)
          break
        case DeploymentType.KUBERNETES:
          await this.deployWithKubernetes(deploymentId, template, input)
          break
        default:
          throw new Error(`Unsupported deployment type: ${input.deploymentType}`)
      }

      // Update status to RUNNING
      await db.query(
        `UPDATE deployments SET status = $1, completed_at = $2 WHERE id = $3`,
        [DeploymentStatus.RUNNING, new Date(), deploymentId]
      )

      await this.logDeployment(deploymentId, 'INFO', 'Deployment completed successfully')
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error)
      await db.query(
        `UPDATE deployments SET status = $1, error_message = $2, completed_at = $3 WHERE id = $4`,
        [DeploymentStatus.FAILED, errorMessage, new Date(), deploymentId]
      )

      await this.logDeployment(deploymentId, 'ERROR', `Deployment failed: ${errorMessage}`, {
        error: errorMessage,
      })

      throw error
    }
  }

  /**
   * Deploy with Terraform
   */
  private async deployWithTerraform(
    deploymentId: string,
    template: Template,
    input: CreateDeploymentInput
  ): Promise<void> {
    await this.logDeployment(deploymentId, 'INFO', 'Executing Terraform deployment')

    const db = getDb()
    const workingDir = join(tmpdir(), `terraform-${deploymentId}`)

    try {
      // Render Terraform content
      const terraformContent = await terraformRenderer.render(template, {
        parameters: input.parameters,
        region: input.region,
        tags: input.tags,
      })

      // Write Terraform files
      await terraformExecutor.writeFiles(workingDir, {
        'main.tf': terraformContent,
        'variables.tf': template.content.includes('variable') ? '' : '# Variables defined in main.tf',
      })

      // Validate
      const validation = await terraformExecutor.validate({ workingDirectory: workingDir })
      if (!validation.valid) {
        throw new Error(`Terraform validation failed: ${validation.errors.join(', ')}`)
      }

      // Initialize
      await this.logDeployment(deploymentId, 'INFO', 'Initializing Terraform...')
      await terraformExecutor.init({
        workingDirectory: workingDir,
      })

      // Plan
      await this.logDeployment(deploymentId, 'INFO', 'Planning Terraform changes...')
      const planFile = join(workingDir, 'plan.tfplan')
      await terraformExecutor.plan(
        {
          workingDirectory: workingDir,
          variables: input.parameters,
        },
        planFile
      )

      // Apply
      await this.logDeployment(deploymentId, 'INFO', 'Applying Terraform changes...')
      await terraformExecutor.apply(
        {
          workingDirectory: workingDir,
        },
        planFile,
        true // auto-approve
      )

      // Get outputs
      const outputs = await terraformExecutor.output({ workingDirectory: workingDir })
      const state = await terraformExecutor.state({ workingDirectory: workingDir })

      // Store outputs and state
      await db.query(
        `UPDATE deployments SET outputs = $1, terraform_state = $2 WHERE id = $3`,
        [JSON.stringify(outputs), JSON.stringify(state), deploymentId]
      )

      await this.logDeployment(deploymentId, 'INFO', 'Terraform deployment completed successfully')
    } catch (error) {
      await this.logDeployment(deploymentId, 'ERROR', `Terraform deployment failed: ${error instanceof Error ? error.message : String(error)}`)
      throw error
    } finally {
      // Cleanup working directory (optional - keep for debugging)
      // await fs.rm(workingDir, { recursive: true, force: true })
    }
  }

  /**
   * Deploy with Helm
   */
  private async deployWithHelm(
    deploymentId: string,
    template: Template,
    input: CreateDeploymentInput
  ): Promise<void> {
    await this.logDeployment(deploymentId, 'INFO', 'Executing Helm deployment')

    try {
      const { helmDeploymentService } = await import('./helm-deployment.js')
      const deployment = await this.getDeployment({ user: { id: 'system' }, tenant: { id: 'system' } } as Context, deploymentId)
      if (!deployment) throw new Error('Deployment not found')

      // Render Helm values
      const { templateEngine } = await import('./template-engine.js')
      const values = templateEngine.renderToHelm(template, {
        parameters: input.parameters,
        region: input.region,
      })

      await helmDeploymentService.deploy(
        deployment,
        template,
        {
          chartName: (template.metadata as any)?.chartName || 'default-chart',
          chartVersion: (template.metadata as any)?.chartVersion,
          namespace: (input.parameters as any)?.namespace || 'default',
          values,
          releaseName: deployment.name,
        }
      )

      await this.logDeployment(deploymentId, 'INFO', 'Helm deployment completed')
    } catch (error) {
      await this.logDeployment(deploymentId, 'ERROR', `Helm deployment failed: ${error instanceof Error ? error.message : String(error)}`)
      throw error
    }
  }

  /**
   * Deploy with Ansible
   */
  private async deployWithAnsible(
    deploymentId: string,
    template: Template,
    input: CreateDeploymentInput
  ): Promise<void> {
    await this.logDeployment(deploymentId, 'INFO', 'Executing Ansible deployment')

    const workingDir = join(tmpdir(), `ansible-${deploymentId}`)

    try {
      await fs.mkdir(workingDir, { recursive: true })

      // Parse playbook from template
      const playbook = JSON.parse(template.content)

      // Create inventory if provided
      if ((input.parameters as any)?.inventory) {
        const inventoryPath = join(workingDir, 'inventory.ini')
        await ansibleExecutor.createInventory(inventoryPath, (input.parameters as any).inventory)
      }

      // Create playbook file
      const playbookPath = join(workingDir, 'playbook.yml')
      await ansibleExecutor.createPlaybook(playbookPath, playbook)

      // Run playbook
      await ansibleExecutor.runPlaybook({
        playbook: playbookPath,
        inventory: (input.parameters as any)?.inventory ? join(workingDir, 'inventory.ini') : undefined,
        extraVars: input.parameters,
      })

      await this.logDeployment(deploymentId, 'INFO', 'Ansible deployment completed')
    } catch (error) {
      await this.logDeployment(deploymentId, 'ERROR', `Ansible deployment failed: ${error instanceof Error ? error.message : String(error)}`)
      throw error
    }
  }

  /**
   * Deploy with Kubernetes
   */
  private async deployWithKubernetes(
    deploymentId: string,
    template: Template,
    input: CreateDeploymentInput
  ): Promise<void> {
    await this.logDeployment(deploymentId, 'INFO', 'Executing Kubernetes deployment')

    try {
      const deployment = await this.getDeployment({ user: { id: 'system' } } as Context, deploymentId)
      if (!deployment) throw new Error('Deployment not found')

      // Parse Kubernetes resources from template
      const resources = (template.metadata as any)?.resources || []

      await k8sOrchestrator.deploy(deployment, {
        namespace: (input.parameters as any)?.namespace || 'default',
        resources,
      })

      await this.logDeployment(deploymentId, 'INFO', 'Kubernetes deployment completed')
    } catch (error) {
      await this.logDeployment(deploymentId, 'ERROR', `Kubernetes deployment failed: ${error instanceof Error ? error.message : String(error)}`)
      throw error
    }
  }

  /**
   * Get deployment by ID
   */
  async getDeployment(context: Context, id: string): Promise<Deployment | null> {
    const db = getDb()
    const result = await db.query(`SELECT * FROM deployments WHERE id = $1`, [id])
    if (result.rows.length === 0) return null

    const deployment = this.mapDeployment(result.rows[0])

    // Check tenant access
    if (context.tenant?.id !== deployment.tenantId && context.user?.role !== 'ADMIN') {
      return null
    }

    return deployment
  }

  /**
   * Get deployments for tenant
   */
  async getDeployments(
    context: Context,
    filter?: {
      status?: DeploymentStatus
      region?: string
      limit?: number
      offset?: number
    }
  ): Promise<Deployment[]> {
    const db = getDb()
    const conditions: string[] = []
    const params: any[] = []
    let paramIndex = 1

    // Filter by tenant
    if (context.tenant?.id) {
      conditions.push(`tenant_id = $${paramIndex}`)
      params.push(context.tenant.id)
      paramIndex++
    } else if (context.user?.role !== 'ADMIN') {
      throw new Error('Tenant context required')
    }

    if (filter?.status) {
      conditions.push(`status = $${paramIndex}`)
      params.push(filter.status)
      paramIndex++
    }

    if (filter?.region) {
      conditions.push(`region = $${paramIndex}`)
      params.push(filter.region)
      paramIndex++
    }

    const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
    const limit = filter?.limit || 50
    const offset = filter?.offset || 0

    params.push(limit, offset)
    const limitClause = `LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`

    const query = `
      SELECT * FROM deployments
      ${whereClause}
      ORDER BY created_at DESC
      ${limitClause}
    `

    const result = await db.query(query, params)
    return result.rows.map(this.mapDeployment)
  }

  /**
   * Get deployment logs
   */
  async getDeploymentLogs(
    context: Context,
    deploymentId: string,
    limit: number = 100
  ): Promise<DeploymentLog[]> {
    // Verify access
    const deployment = await this.getDeployment(context, deploymentId)
    if (!deployment) {
      throw new Error('Deployment not found or access denied')
    }

    const db = getDb()
    const result = await db.query(
      `SELECT * FROM deployment_logs 
       WHERE deployment_id = $1 
       ORDER BY created_at DESC 
       LIMIT $2`,
      [deploymentId, limit]
    )

    return result.rows.map(this.mapDeploymentLog)
  }

  /**
   * Log deployment message
   */
  private async logDeployment(
    deploymentId: string,
    level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR',
    message: string,
    metadata: Record<string, any> = {}
  ): Promise<void> {
    const db = getDb()
    await db.query(
      `INSERT INTO deployment_logs (deployment_id, level, message, metadata)
       VALUES ($1, $2, $3, $4)`,
      [deploymentId, level, message, JSON.stringify(metadata)]
    )
  }

  /**
   * Record deployment event
   */
  private async recordEvent(
    deploymentId: string,
    eventType: string,
    eventData: Record<string, any>,
    createdBy?: string
  ): Promise<void> {
    const db = getDb()
    await db.query(
      `INSERT INTO deployment_events (deployment_id, event_type, event_data, created_by)
       VALUES ($1, $2, $3, $4)`,
      [deploymentId, eventType, JSON.stringify(eventData), createdBy || null]
    )
  }

  /**
   * Delete deployment
   */
  async deleteDeployment(context: Context, id: string): Promise<boolean> {
    const deployment = await this.getDeployment(context, id)
    if (!deployment) {
      throw new Error('Deployment not found or access denied')
    }

    const db = getDb()

    // Update status to DELETING
    await db.query(
      `UPDATE deployments SET status = $1 WHERE id = $2`,
      [DeploymentStatus.DELETING, id]
    )

    await this.logDeployment(id, 'INFO', 'Deployment deletion started')

    // Perform actual deletion based on deployment type
    // This would tear down resources

    // Update status to DELETED
    await db.query(
      `UPDATE deployments SET status = $1, completed_at = $2 WHERE id = $3`,
      [DeploymentStatus.DELETED, new Date(), id]
    )

    await this.logDeployment(id, 'INFO', 'Deployment deleted')

    return true
  }

  // Mapper functions
  private mapDeployment(row: any): Deployment {
    return {
      id: row.id,
      name: row.name,
      productId: row.product_id,
      productVersionId: row.product_version_id,
      templateId: row.template_id,
      templateVersionId: row.template_version_id,
      tenantId: row.tenant_id,
      region: row.region,
      status: row.status as DeploymentStatus,
      deploymentType: row.deployment_type as DeploymentType,
      parameters: row.parameters || {},
      renderedContent: row.rendered_content,
      terraformState: row.terraform_state,
      outputs: row.outputs || {},
      errorMessage: row.error_message,
      createdBy: row.created_by,
      startedAt: row.started_at,
      completedAt: row.completed_at,
      createdAt: row.created_at,
      updatedAt: row.updated_at,
    }
  }

  private mapDeploymentLog(row: any): DeploymentLog {
    return {
      id: row.id,
      deploymentId: row.deployment_id,
      level: row.level,
      message: row.message,
      metadata: row.metadata || {},
      createdAt: row.created_at,
    }
  }
}

export const deploymentService = new DeploymentService()

