import fs from 'fs/promises'
import fsSync from 'fs'
import path from 'path'
import crypto from 'crypto'
import os from 'os'
import type { EnvVariable, DatabaseVariable, HistoryEntry, Snapshot } from '../types.js'

export default class EnvDatabase {
  private dbPath: string
  private data: {
    variables: DatabaseVariable[]
    history: HistoryEntry[]
    snapshots: Snapshot[]
    auth: {
      passwordHash?: string
      salt?: string
      isAuthenticated: boolean
      lastAuth?: number
    }
    hotReload?: {
      enabled: boolean
      autoReload: boolean
      reloadDelay: number
      wsPort: number
      notifyOnly: boolean
    }
    metadata?: Record<string, string>
  }
  private encryptionKey: string | null = null

  constructor(dbPath?: string) {
    const projectRoot = process.env.PROJECT_ROOT || process.cwd()
    this.dbPath = dbPath || path.join(projectRoot, 'env-data.json')
    this.data = {
      variables: [],
      history: [],
      snapshots: [],
      auth: {
        isAuthenticated: false
      },
      metadata: {}
    }
    this.loadData()
    this.loadMasterPasswordIfNeeded()
  }

  private async loadData(): Promise<void> {
    try {
      const fileContent = await fs.readFile(this.dbPath, 'utf-8')
      this.data = JSON.parse(fileContent)
    } catch (error) {
      // File doesn't exist or is invalid, start with empty data
      await this.saveData()
    }
  }

  private async saveData(): Promise<void> {
    try {
      await fs.writeFile(this.dbPath, JSON.stringify(this.data, null, 2))
    } catch (error) {
      console.error('Failed to save database:', error)
      throw error
    }
  }

  private generateSalt(): string {
    return crypto.randomBytes(16).toString('hex')
  }

  private hashPassword(password: string, salt: string): string {
    return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex')
  }

  private deriveEncryptionKey(password: string, salt: string): string {
    return crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha512').toString('hex')
  }

  private encrypt(text: string): string {
    if (!this.encryptionKey) {
      // If no encryption key (no password set), return the value as-is
      // This allows sensitive variables to be stored without encryption when no password is set
      return text
    }

    // Ensure encryption key is a Buffer of the right length
    const keyBuffer = typeof this.encryptionKey === 'string' 
      ? Buffer.from(this.encryptionKey, 'hex').slice(0, 32)
      : this.encryptionKey

    const iv = crypto.randomBytes(16)
    const cipher = crypto.createCipheriv('aes-256-cbc', keyBuffer, iv)
    let encrypted = cipher.update(text, 'utf8', 'hex')
    encrypted += cipher.final('hex')
    return iv.toString('hex') + ':' + encrypted
  }

  private decrypt(encryptedText: string): string {
    if (!this.encryptionKey) {
      // If no encryption key, assume the value is not encrypted
      return encryptedText
    }

    // Check if the text looks encrypted (has the IV prefix)
    if (!encryptedText.includes(':')) {
      // Not encrypted, return as-is
      return encryptedText
    }

    // Ensure encryption key is a Buffer of the right length
    const keyBuffer = typeof this.encryptionKey === 'string' 
      ? Buffer.from(this.encryptionKey, 'hex').slice(0, 32)
      : this.encryptionKey

    const [ivHex, encrypted] = encryptedText.split(':')
    const decipher = crypto.createDecipheriv('aes-256-cbc', keyBuffer, Buffer.from(ivHex, 'hex'))
    let decrypted = decipher.update(encrypted, 'hex', 'utf8')
    decrypted += decipher.final('utf8')
    return decrypted
  }

  private loadSecureCredentials(): { passwordHash: string; salt: string; encryptionKey: string } | null {
    try {
      const credPath = path.join(os.homedir(), '.env-manager', 'credentials.json')
      if (fsSync.existsSync(credPath)) {
        const credentials = JSON.parse(fsSync.readFileSync(credPath, 'utf-8'))
        return credentials
      }
    } catch (error) {
      console.error('Failed to load secure credentials:', error)
    }
    return null
  }

  private loadMasterPasswordIfNeeded(): void {
    // If no project password is set but we need encryption for sensitive vars,
    // try to load the master password
    if (!this.data.auth.passwordHash && !this.data.auth.salt) {
      const masterCreds = this.loadSecureCredentials()
      if (masterCreds) {
        // Use master password's encryption key for this project
        this.encryptionKey = masterCreds.encryptionKey
        // Mark as authenticated since we have master password
        this.data.auth.isAuthenticated = true
      }
    }
  }

  authenticate(password: string): boolean {
    try {
      // First try to load credentials from secure storage
      const secureCredentials = this.loadSecureCredentials()
      
      if (secureCredentials) {
        // Use secure credentials from home directory
        const hash = this.hashPassword(password, secureCredentials.salt)
        if (hash === secureCredentials.passwordHash) {
          this.data.auth.isAuthenticated = true
          this.data.auth.lastAuth = Date.now()
          // Use the stored encryption key instead of deriving from password
          this.encryptionKey = secureCredentials.encryptionKey
          this.saveData()
          return true
        }
        return false
      }
      
      // Fallback to old method for backward compatibility
      // First time setup
      if (!this.data.auth.passwordHash || !this.data.auth.salt) {
        const salt = this.generateSalt()
        const hash = this.hashPassword(password, salt)

        this.data.auth.salt = salt
        this.data.auth.passwordHash = hash
        this.data.auth.isAuthenticated = true
        this.data.auth.lastAuth = Date.now()

        this.encryptionKey = this.deriveEncryptionKey(password, salt)
        this.saveData()
        return true
      }

      // Verify existing password (old method)
      const hash = this.hashPassword(password, this.data.auth.salt!)
      if (hash === this.data.auth.passwordHash) {
        this.data.auth.isAuthenticated = true
        this.data.auth.lastAuth = Date.now()
        this.encryptionKey = this.deriveEncryptionKey(password, this.data.auth.salt!)
        this.saveData()
        return true
      }

      return false
    } catch (error) {
      console.error('Authentication error:', error)
      return false
    }
  }

  isAuthenticated(): boolean {
    // If no password is set, consider it always authenticated
    if (!this.data.auth.passwordHash && !this.data.auth.salt) {
      return true
    }
    // Otherwise check both auth status and encryption key
    return this.data.auth.isAuthenticated && !!this.encryptionKey
  }

  logout(): void {
    this.data.auth.isAuthenticated = false
    this.encryptionKey = null
    this.saveData()
  }

  setVariable(name: string, value: string, metadata: Partial<DatabaseVariable> = {}): DatabaseVariable {
    if (!this.isAuthenticated()) {
      throw new Error('Not authenticated')
    }

    const now = new Date().toISOString()
    
    // Find existing variable considering branch
    const branch = metadata.branch || 'main'
    const existingIndex = this.data.variables.findIndex(v => 
      v.name === name && (v.branch || 'main') === branch
    )

    const variable: DatabaseVariable = {
      name,
      value: metadata.sensitive ? this.encrypt(value) : value,
      category: metadata.category || 'other',
      description: metadata.description || '',
      sensitive: metadata.sensitive || false,
      encrypted: (metadata.sensitive || false) && !!this.encryptionKey,
      branch: metadata.branch,
      environment: metadata.environment,
      createdAt: existingIndex >= 0 ? this.data.variables[existingIndex].createdAt : now,
      updatedAt: now
    }

    // Add to history
    this.data.history.push({
      id: crypto.randomUUID(),
      action: existingIndex >= 0 ? 'update' : 'create',
      variableName: name,
      oldValue: existingIndex >= 0 ? this.data.variables[existingIndex].value : undefined,
      newValue: variable.value,
      timestamp: now
    })

    if (existingIndex >= 0) {
      this.data.variables[existingIndex] = variable
    } else {
      this.data.variables.push(variable)
    }

    this.saveData()
    return variable
  }

  updateVariable(name: string, value: string, metadata: Partial<EnvVariable> = {}): DatabaseVariable {
    return this.setVariable(name, value, metadata)
  }

  getVariable(name: string): DatabaseVariable | null {
    if (!this.isAuthenticated()) {
      throw new Error('Not authenticated')
    }

    const variable = this.data.variables.find(v => v.name === name)
    if (!variable) return null

    // Decrypt if necessary
    if (variable.encrypted && variable.sensitive) {
      return {
        ...variable,
        value: this.decrypt(variable.value)
      }
    }

    return variable
  }

  getAllVariables(branch?: string): DatabaseVariable[] {
    if (!this.isAuthenticated()) {
      throw new Error('Not authenticated')
    }

    const targetBranch = branch || 'main'
    
    // Filter variables by branch, with fallback to main branch
    const branchVariables = this.data.variables.filter(v => {
      const varBranch = v.branch || 'main'
      return varBranch === targetBranch || (targetBranch !== 'main' && varBranch === 'main')
    })

    // Deduplicate - branch-specific overrides main
    const variableMap = new Map<string, DatabaseVariable>()
    
    // First add main branch variables
    branchVariables
      .filter(v => (v.branch || 'main') === 'main')
      .forEach(v => variableMap.set(v.name, v))
    
    // Then override with branch-specific
    if (targetBranch !== 'main') {
      branchVariables
        .filter(v => v.branch === targetBranch)
        .forEach(v => variableMap.set(v.name, v))
    }

    return Array.from(variableMap.values()).map(variable => {
      if (variable.encrypted && variable.sensitive) {
        try {
          return {
            ...variable,
            value: this.decrypt(variable.value)
          }
        } catch (error) {
          console.error(`Failed to decrypt variable ${variable.name}:`, error)
          return {
            ...variable,
            value: '[DECRYPTION_FAILED]'
          }
        }
      }
      return variable
    })
  }

  getVariablesByBranch(branch: string): DatabaseVariable[] {
    return this.getAllVariables(branch)
  }

  getBranches(): string[] {
    const branches = new Set<string>()
    this.data.variables.forEach(v => {
      branches.add(v.branch || 'main')
    })
    return Array.from(branches)
  }

  deleteVariable(name: string): boolean {
    if (!this.isAuthenticated()) {
      throw new Error('Not authenticated')
    }

    const index = this.data.variables.findIndex(v => v.name === name)
    if (index === -1) return false

    const variable = this.data.variables[index]

    // Add to history
    this.data.history.push({
      id: crypto.randomUUID(),
      action: 'delete',
      variableName: name,
      oldValue: variable.value,
      timestamp: new Date().toISOString()
    })

    this.data.variables.splice(index, 1)
    this.saveData()
    return true
  }

  getHistory(): HistoryEntry[] {
    if (!this.isAuthenticated()) {
      throw new Error('Not authenticated')
    }
    return this.data.history.slice().reverse() // Most recent first
  }

  createSnapshot(name: string, description?: string): Snapshot {
    if (!this.isAuthenticated()) {
      throw new Error('Not authenticated')
    }

    const snapshot: Snapshot = {
      id: crypto.randomUUID(),
      name,
      description: description || '',
      variables: JSON.parse(JSON.stringify(this.data.variables)),
      createdAt: new Date().toISOString()
    }

    this.data.snapshots.push(snapshot)
    this.saveData()
    return snapshot
  }

  getSnapshots(): Snapshot[] {
    if (!this.isAuthenticated()) {
      throw new Error('Not authenticated')
    }
    return this.data.snapshots.slice().reverse() // Most recent first
  }

  restoreSnapshot(snapshotId: string): boolean {
    if (!this.isAuthenticated()) {
      throw new Error('Not authenticated')
    }

    const snapshot = this.data.snapshots.find(s => s.id === snapshotId)
    if (!snapshot) return false

    // Create a backup snapshot first
    this.createSnapshot(`Backup before restore ${snapshot.name}`, 'Auto-created before snapshot restore')

    // Restore variables
    this.data.variables = JSON.parse(JSON.stringify(snapshot.variables))

    // Add to history
    this.data.history.push({
      id: crypto.randomUUID(),
      action: 'restore',
      variableName: `Snapshot: ${snapshot.name}`,
      timestamp: new Date().toISOString()
    })

    this.saveData()
    return true
  }

  deleteSnapshot(snapshotId: string): boolean {
    if (!this.isAuthenticated()) {
      throw new Error('Not authenticated')
    }

    const index = this.data.snapshots.findIndex(s => s.id === snapshotId)
    if (index === -1) return false

    this.data.snapshots.splice(index, 1)
    this.saveData()
    return true
  }

  generateEnvFile(): string {
    if (!this.isAuthenticated()) {
      throw new Error('Not authenticated')
    }

    const variables = this.getAllVariables()
    const lines: string[] = []

    // Group by category
    const categories = new Map<string, DatabaseVariable[]>()
    variables.forEach(variable => {
      const category = variable.category || 'other'
      if (!categories.has(category)) {
        categories.set(category, [])
      }
      categories.get(category)!.push(variable)
    })

    // Generate file content
    categories.forEach((vars, category) => {
      lines.push(`# ${category.toUpperCase()}`)
      vars.forEach(variable => {
        if (variable.description) {
          lines.push(`# ${variable.description}`)
        }
        lines.push(`${variable.name}=${variable.value}`)
      })
      lines.push('')
    })

    return lines.join('\n')
  }

  generateEnvExample(): string {
    if (!this.isAuthenticated()) {
      throw new Error('Not authenticated')
    }

    const variables = this.getAllVariables()
    const lines: string[] = []

    // Group by category
    const categories = new Map<string, DatabaseVariable[]>()
    variables.forEach(variable => {
      const category = variable.category || 'other'
      if (!categories.has(category)) {
        categories.set(category, [])
      }
      categories.get(category)!.push(variable)
    })

    // Generate example file content
    categories.forEach((vars, category) => {
      lines.push(`# ${category.toUpperCase()}`)
      vars.forEach(variable => {
        if (variable.description) {
          lines.push(`# ${variable.description}`)
        }
        const placeholder = variable.sensitive ?
          'your_secret_here' :
          variable.value || 'your_value_here'
        lines.push(`${variable.name}=${placeholder}`)
      })
      lines.push('')
    })

    return lines.join('\n')
  }

  // Hot-reload settings management
  getHotReloadSettings(): any {
    if (!this.data.hotReload) {
      this.data.hotReload = {
        enabled: true,
        autoReload: true,
        reloadDelay: 1000,
        wsPort: 3002,
        notifyOnly: false
      }
      this.saveData()
    }
    return this.data.hotReload
  }

  setHotReloadSettings(settings: any): void {
    this.data.hotReload = {
      ...this.getHotReloadSettings(),
      ...settings
    }
    this.saveData()
  }

  // Metadata methods for draft/publish system
  getMetadata(key: string): string | undefined {
    return this.data.metadata?.[key]
  }

  setMetadata(key: string, value: string): void {
    if (!this.data.metadata) {
      this.data.metadata = {}
    }
    this.data.metadata[key] = value
    this.saveData()
  }

  deleteMetadata(key: string): void {
    if (this.data.metadata) {
      delete this.data.metadata[key]
      this.saveData()
    }
  }

  getAllMetadata(): Record<string, string> {
    return { ...(this.data.metadata || {}) }
  }
}
