import { prisma } from '@/lib/prisma'
import { JobStatus, OrderStatus, LogLevel } from '@prisma/client'
import crypto from 'crypto'
import { SSH_PORT_AFTER_PROVISION } from '@/lib/ssh/constants'
import { credentialService } from '@/lib/services/credential-service'

const toLogLevel = (level: 'info' | 'warn' | 'error'): LogLevel => {
  const map: Record<'info' | 'warn' | 'error', LogLevel> = {
    info: LogLevel.INFO,
    warn: LogLevel.WARN,
    error: LogLevel.ERROR,
  }
  return map[level]
}

// Retry delays in seconds: 1min, 5min, 15min
const RETRY_DELAYS = [60, 300, 900]

export interface JobConfig {
  server: {
    ip: string
    port: number
    rootPassword: string
  }
  customer: string
  domain: string
  companyName: string
  licenseKey: string
  dashboardTier: string
  tools: string[]
  keycloak?: {
    realm: string
    clients: Array<{ clientId: string; public: boolean }>
  }
}

export class JobService {
  /**
   * Create a new provisioning job for an order
   */
  async createJobForOrder(orderId: string): Promise<string> {
    const order = await prisma.order.findUnique({
      where: { id: orderId },
      include: {
        user: {
          include: {
            subscriptions: {
              where: { status: 'ACTIVE' },
              orderBy: { createdAt: 'desc' },
              take: 1,
            },
          },
        },
      },
    })

    if (!order) {
      throw new Error(`Order ${orderId} not found`)
    }

    if (!order.serverIp || !order.serverPasswordEncrypted) {
      throw new Error(`Order ${orderId} missing server credentials`)
    }

    // Build config snapshot
    const configSnapshot: JobConfig = {
      server: {
        ip: order.serverIp,
        port: order.sshPort,
        rootPassword: this.decryptPassword(order.serverPasswordEncrypted),
      },
      customer: order.user.email.split('@')[0],
      domain: order.domain,
      companyName: order.user.company || order.user.name || 'Customer',
      licenseKey: await this.generateLicenseKey(order.id),
      dashboardTier: order.tier,
      tools: order.tools,
      keycloak: {
        realm: 'letsbe',
        clients: [{ clientId: 'dashboard', public: true }],
      },
    }

    // Generate runner token
    const runnerToken = crypto.randomBytes(32).toString('hex')
    const runnerTokenHash = crypto.createHash('sha256').update(runnerToken).digest('hex')

    const job = await prisma.provisioningJob.create({
      data: {
        orderId,
        jobType: 'provision',
        configSnapshot: configSnapshot as object,
        runnerTokenHash,
      },
    })

    // Update order status
    await prisma.order.update({
      where: { id: orderId },
      data: {
        status: OrderStatus.PROVISIONING,
        provisioningStartedAt: new Date(),
      },
    })

    // Return job ID with runner token (token is only returned once)
    return JSON.stringify({ jobId: job.id, runnerToken })
  }

  /**
   * Claim the next available job for processing
   * Uses SELECT FOR UPDATE SKIP LOCKED pattern for concurrent workers
   */
  async claimNextJob(workerId: string): Promise<{
    jobId: string
    config: JobConfig
    runnerToken: string
  } | null> {
    // Use a transaction with row-level locking
    const result = await prisma.$transaction(async (tx) => {
      // Find next pending job, skip locked rows
      const jobs = await tx.$queryRaw<Array<{ id: string }>>`
        SELECT id FROM "ProvisioningJob"
        WHERE status = 'PENDING'
        AND (next_retry_at IS NULL OR next_retry_at <= NOW())
        ORDER BY priority DESC, created_at ASC
        LIMIT 1
        FOR UPDATE SKIP LOCKED
      `

      if (jobs.length === 0) {
        return null
      }

      const jobId = jobs[0].id

      // Generate new runner token for this claim
      const runnerToken = crypto.randomBytes(32).toString('hex')
      const runnerTokenHash = crypto.createHash('sha256').update(runnerToken).digest('hex')

      // Update job as claimed
      const job = await tx.provisioningJob.update({
        where: { id: jobId },
        data: {
          status: JobStatus.RUNNING,
          claimedAt: new Date(),
          claimedBy: workerId,
          runnerTokenHash,
        },
      })

      return {
        jobId: job.id,
        config: job.configSnapshot as unknown as JobConfig,
        runnerToken,
      }
    })

    return result
  }

  /**
   * Verify a runner token for a job
   */
  async verifyRunnerToken(jobId: string, token: string): Promise<boolean> {
    const job = await prisma.provisioningJob.findUnique({
      where: { id: jobId },
      select: { runnerTokenHash: true },
    })

    if (!job || !job.runnerTokenHash) {
      return false
    }

    const providedHash = crypto.createHash('sha256').update(token).digest('hex')
    return crypto.timingSafeEqual(
      Buffer.from(job.runnerTokenHash),
      Buffer.from(providedHash)
    )
  }

  /**
   * Add a log entry for a job
   */
  async addLog(
    jobId: string,
    level: 'info' | 'warn' | 'error',
    message: string,
    step?: string,
    progress?: number
  ): Promise<void> {
    const dbLevel = toLogLevel(level)

    await prisma.jobLog.create({
      data: {
        jobId,
        level: dbLevel,
        message,
        step,
        progress,
      },
    })

    // Also create a provisioning log on the order for easy access
    const job = await prisma.provisioningJob.findUnique({
      where: { id: jobId },
      select: { orderId: true },
    })

    if (job) {
      await prisma.provisioningLog.create({
        data: {
          orderId: job.orderId,
          level: dbLevel,
          message,
          step,
        },
      })
    }
  }

  /**
   * Get logs for a job
   */
  async getLogs(jobId: string, since?: Date): Promise<Array<{
    id: string
    timestamp: Date
    level: string
    message: string
    step: string | null
    progress: number | null
  }>> {
    const where: { jobId: string; timestamp?: { gt: Date } } = { jobId }
    if (since) {
      where.timestamp = { gt: since }
    }

    return prisma.jobLog.findMany({
      where,
      orderBy: { timestamp: 'asc' },
      select: {
        id: true,
        timestamp: true,
        level: true,
        message: true,
        step: true,
        progress: true,
      },
    })
  }

  /**
   * Complete a job successfully
   */
  async completeJob(jobId: string, result?: object): Promise<void> {
    const job = await prisma.provisioningJob.update({
      where: { id: jobId },
      data: {
        status: JobStatus.COMPLETED,
        completedAt: new Date(),
        result: result || {},
      },
    })

    // Update order status and SSH port (Ansible changes port to 22022)
    await prisma.order.update({
      where: { id: job.orderId },
      data: {
        status: OrderStatus.FULFILLED,
        completedAt: new Date(),
        // Update SSH port after provisioning (Ansible hardens SSH to use custom port)
        sshPort: SSH_PORT_AFTER_PROVISION,
        // Clear sensitive data
        serverPasswordEncrypted: null,
      },
    })
  }

  /**
   * Fail a job - will retry if attempts remaining
   */
  async failJob(jobId: string, error: string): Promise<{ willRetry: boolean; nextRetryAt?: Date }> {
    const job = await prisma.provisioningJob.findUnique({
      where: { id: jobId },
    })

    if (!job) {
      throw new Error(`Job ${jobId} not found`)
    }

    const nextAttempt = job.attempt + 1
    const willRetry = nextAttempt <= job.maxAttempts

    if (willRetry) {
      // Schedule retry with exponential backoff
      const delaySeconds = RETRY_DELAYS[Math.min(job.attempt - 1, RETRY_DELAYS.length - 1)]
      const nextRetryAt = new Date(Date.now() + delaySeconds * 1000)

      await prisma.provisioningJob.update({
        where: { id: jobId },
        data: {
          status: JobStatus.PENDING,
          attempt: nextAttempt,
          nextRetryAt,
          claimedAt: null,
          claimedBy: null,
          runnerTokenHash: null,
        },
      })

      await this.addLog(jobId, 'warn', `Job failed, will retry (attempt ${nextAttempt}/${job.maxAttempts}): ${error}`, 'retry')

      return { willRetry: true, nextRetryAt }
    } else {
      // Max retries exceeded - mark as dead
      await prisma.provisioningJob.update({
        where: { id: jobId },
        data: {
          status: JobStatus.DEAD,
          completedAt: new Date(),
          error,
        },
      })

      // Update order status to failed
      await prisma.order.update({
        where: { id: job.orderId },
        data: {
          status: OrderStatus.FAILED,
          failureReason: error,
        },
      })

      await this.addLog(jobId, 'error', `Job failed permanently after ${job.maxAttempts} attempts: ${error}`, 'dead')

      return { willRetry: false }
    }
  }

  /**
   * Get job status
   */
  async getJobStatus(jobId: string): Promise<{
    status: JobStatus
    attempt: number
    maxAttempts: number
    progress?: number
    error?: string
  } | null> {
    const job = await prisma.provisioningJob.findUnique({
      where: { id: jobId },
      select: {
        status: true,
        attempt: true,
        maxAttempts: true,
        error: true,
      },
    })

    if (!job) {
      return null
    }

    // Get latest progress from logs
    const latestLog = await prisma.jobLog.findFirst({
      where: { jobId, progress: { not: null } },
      orderBy: { timestamp: 'desc' },
      select: { progress: true },
    })

    return {
      ...job,
      progress: latestLog?.progress || undefined,
      error: job.error || undefined,
    }
  }

  /**
   * Get pending job count
   */
  async getPendingJobCount(): Promise<number> {
    return prisma.provisioningJob.count({
      where: {
        status: JobStatus.PENDING,
        OR: [
          { nextRetryAt: null },
          { nextRetryAt: { lte: new Date() } },
        ],
      },
    })
  }

  /**
   * Get running job count
   */
  async getRunningJobCount(): Promise<number> {
    return prisma.provisioningJob.count({
      where: { status: JobStatus.RUNNING },
    })
  }

  // Helper methods
  private decryptPassword(encrypted: string): string {
    // Try new encryption format first (CREDENTIAL_ENCRYPTION_KEY)
    try {
      return credentialService.decrypt(encrypted)
    } catch {
      // Fall back to legacy format (ENCRYPTION_KEY) for migration
      return credentialService.decryptLegacy(encrypted)
    }
  }

  private async generateLicenseKey(orderId: string): Promise<string> {
    // Generate a unique license key for this order
    const hash = crypto.createHash('sha256').update(orderId + Date.now()).digest('hex')
    return `lb_inst_${hash.slice(0, 40)}`
  }
}

// Singleton instance
export const jobService = new JobService()
