<?php
/* Copyright (C) 2026 Alles Watt lauft
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 */

/**
 * Class AnlageBackup
 * Handles backup and restore of all installation data
 */
class AnlageBackup
{
	public $db;
	public $error;
	public $errors = array();

	// Tables to backup (in order for foreign key constraints)
	private $tables = array(
		'kundenkarte_anlage_system',
		'kundenkarte_anlage_type',
		'kundenkarte_anlage_type_field',
		'kundenkarte_customer_system',
		'kundenkarte_anlage',
		'kundenkarte_anlage_files',
		'kundenkarte_anlage_connection',
		'kundenkarte_equipment_panel',
		'kundenkarte_equipment_carrier',
		'kundenkarte_equipment',
		'kundenkarte_medium_type',
		'kundenkarte_busbar_type',
		'kundenkarte_building_type',
	);

	/**
	 * Constructor
	 *
	 * @param DoliDB $db Database handler
	 */
	public function __construct($db)
	{
		$this->db = $db;
	}

	/**
	 * Create a full backup
	 *
	 * @param bool $includeFiles Include uploaded files in backup
	 * @return string|false Path to backup file or false on error
	 */
	public function createBackup($includeFiles = true)
	{
		global $conf;

		$backupDir = $conf->kundenkarte->dir_output.'/backups';
		if (!is_dir($backupDir)) {
			dol_mkdir($backupDir);
		}

		$timestamp = date('Y-m-d_H-i-s');
		$backupName = 'kundenkarte_backup_'.$timestamp;
		$tempDir = $backupDir.'/'.$backupName;

		if (!dol_mkdir($tempDir)) {
			$this->error = 'Cannot create backup directory';
			return false;
		}

		// Export database tables
		$dbData = $this->exportDatabaseTables();
		if ($dbData === false) {
			return false;
		}

		// Save database export as JSON
		$dbFile = $tempDir.'/database.json';
		if (file_put_contents($dbFile, json_encode($dbData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) === false) {
			$this->error = 'Cannot write database file';
			return false;
		}

		// Create metadata file
		$metadata = array(
			'version' => '3.6.0',
			'created' => date('Y-m-d H:i:s'),
			'tables' => array_keys($dbData),
			'record_counts' => array(),
			'includes_files' => $includeFiles,
		);
		foreach ($dbData as $table => $records) {
			$metadata['record_counts'][$table] = count($records);
		}
		file_put_contents($tempDir.'/metadata.json', json_encode($metadata, JSON_PRETTY_PRINT));

		// Copy uploaded files if requested
		if ($includeFiles) {
			$filesDir = $conf->kundenkarte->dir_output.'/anlagen';
			if (is_dir($filesDir)) {
				$this->copyDirectory($filesDir, $tempDir.'/files');
			}
		}

		// Create ZIP archive
		$zipFile = $backupDir.'/'.$backupName.'.zip';
		if (!$this->createZipArchive($tempDir, $zipFile)) {
			$this->error = 'Cannot create ZIP archive';
			return false;
		}

		// Clean up temp directory
		$this->deleteDirectory($tempDir);

		return $zipFile;
	}

	/**
	 * Export all database tables
	 *
	 * @return array|false Array of table data or false on error
	 */
	private function exportDatabaseTables()
	{
		global $conf;

		$data = array();

		foreach ($this->tables as $table) {
			$fullTable = MAIN_DB_PREFIX.$table;

			// Check if table exists
			$sql = "SHOW TABLES LIKE '".$this->db->escape($fullTable)."'";
			$resql = $this->db->query($sql);
			if (!$resql || $this->db->num_rows($resql) == 0) {
				continue; // Skip non-existent tables
			}

			$records = array();
			$sql = "SELECT * FROM ".$fullTable;
			$sql .= " WHERE entity = ".((int) $conf->entity);

			$resql = $this->db->query($sql);
			if ($resql) {
				while ($obj = $this->db->fetch_array($resql)) {
					$records[] = $obj;
				}
				$this->db->free($resql);
			}

			$data[$table] = $records;
		}

		return $data;
	}

	/**
	 * Restore from a backup file
	 *
	 * @param string $backupFile Path to backup ZIP file
	 * @param bool $clearExisting Clear existing data before restore
	 * @return bool True on success, false on error
	 */
	public function restoreBackup($backupFile, $clearExisting = false)
	{
		global $conf, $user;

		if (!file_exists($backupFile)) {
			$this->error = 'Backup file not found';
			return false;
		}

		// Create temp directory for extraction
		$tempDir = $conf->kundenkarte->dir_output.'/backups/restore_'.uniqid();
		if (!dol_mkdir($tempDir)) {
			$this->error = 'Cannot create temp directory';
			return false;
		}

		// Extract ZIP
		$zip = new ZipArchive();
		if ($zip->open($backupFile) !== true) {
			$this->error = 'Cannot open backup file';
			return false;
		}
		$zip->extractTo($tempDir);
		$zip->close();

		// Read metadata
		$metadataFile = $tempDir.'/metadata.json';
		if (!file_exists($metadataFile)) {
			$this->error = 'Invalid backup: metadata.json not found';
			$this->deleteDirectory($tempDir);
			return false;
		}
		$metadata = json_decode(file_get_contents($metadataFile), true);

		// Read database data
		$dbFile = $tempDir.'/database.json';
		if (!file_exists($dbFile)) {
			$this->error = 'Invalid backup: database.json not found';
			$this->deleteDirectory($tempDir);
			return false;
		}
		$dbData = json_decode(file_get_contents($dbFile), true);

		$this->db->begin();

		try {
			// Clear existing data if requested
			if ($clearExisting) {
				$this->clearExistingData();
			}

			// Import database tables (in correct order)
			foreach ($this->tables as $table) {
				if (isset($dbData[$table])) {
					$this->importTable($table, $dbData[$table]);
				}
			}

			// Restore files if included
			if (!empty($metadata['includes_files']) && is_dir($tempDir.'/files')) {
				$filesDir = $conf->kundenkarte->dir_output.'/anlagen';
				if (!is_dir($filesDir)) {
					dol_mkdir($filesDir);
				}
				$this->copyDirectory($tempDir.'/files', $filesDir);
			}

			$this->db->commit();
		} catch (Exception $e) {
			$this->db->rollback();
			$this->error = $e->getMessage();
			$this->deleteDirectory($tempDir);
			return false;
		}

		// Clean up
		$this->deleteDirectory($tempDir);

		return true;
	}

	/**
	 * Clear existing data for this entity
	 */
	private function clearExistingData()
	{
		global $conf;

		// Delete in reverse order to respect foreign keys
		$reverseTables = array_reverse($this->tables);

		foreach ($reverseTables as $table) {
			$fullTable = MAIN_DB_PREFIX.$table;

			// Check if table exists
			$sql = "SHOW TABLES LIKE '".$this->db->escape($fullTable)."'";
			$resql = $this->db->query($sql);
			if (!$resql || $this->db->num_rows($resql) == 0) {
				continue;
			}

			$sql = "DELETE FROM ".$fullTable." WHERE entity = ".((int) $conf->entity);
			$this->db->query($sql);
		}
	}

	/**
	 * Import data into a table
	 *
	 * @param string $table Table name (without prefix)
	 * @param array $records Array of records
	 */
	private function importTable($table, $records)
	{
		global $conf;

		if (empty($records)) {
			return;
		}

		$fullTable = MAIN_DB_PREFIX.$table;

		// Check if table exists
		$sql = "SHOW TABLES LIKE '".$this->db->escape($fullTable)."'";
		$resql = $this->db->query($sql);
		if (!$resql || $this->db->num_rows($resql) == 0) {
			return;
		}

		// Get column info
		$columns = array();
		$sql = "SHOW COLUMNS FROM ".$fullTable;
		$resql = $this->db->query($sql);
		while ($obj = $this->db->fetch_object($resql)) {
			$columns[] = $obj->Field;
		}

		foreach ($records as $record) {
			// Build insert statement
			$fields = array();
			$values = array();

			foreach ($record as $field => $value) {
				if (!in_array($field, $columns)) {
					continue; // Skip unknown columns
				}

				$fields[] = $field;

				if ($value === null) {
					$values[] = 'NULL';
				} elseif (is_numeric($value)) {
					$values[] = $value;
				} else {
					$values[] = "'".$this->db->escape($value)."'";
				}
			}

			if (empty($fields)) {
				continue;
			}

			$sql = "INSERT INTO ".$fullTable." (".implode(', ', $fields).") VALUES (".implode(', ', $values).")";
			$sql .= " ON DUPLICATE KEY UPDATE ";

			$updates = array();
			foreach ($fields as $i => $field) {
				if ($field != 'rowid') {
					$updates[] = $field." = ".$values[$i];
				}
			}
			$sql .= implode(', ', $updates);

			if (!$this->db->query($sql)) {
				throw new Exception('Error importing '.$table.': '.$this->db->lasterror());
			}
		}
	}

	/**
	 * Get list of available backups
	 *
	 * @return array Array of backup info
	 */
	public function getBackupList()
	{
		global $conf;

		$backups = array();
		$backupDir = $conf->kundenkarte->dir_output.'/backups';

		if (!is_dir($backupDir)) {
			return $backups;
		}

		$files = glob($backupDir.'/kundenkarte_backup_*.zip');
		if ($files) {
			foreach ($files as $file) {
				$filename = basename($file);
				// Extract date from filename
				if (preg_match('/kundenkarte_backup_(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})\.zip/', $filename, $matches)) {
					$date = str_replace('_', ' ', $matches[1]);
					$date = str_replace('-', ':', substr($date, 11));
					$date = substr($matches[1], 0, 10).' '.$date;

					$backups[] = array(
						'file' => $file,
						'filename' => $filename,
						'date' => $date,
						'size' => filesize($file),
					);
				}
			}
		}

		// Sort by date descending
		usort($backups, function ($a, $b) {
			return strcmp($b['date'], $a['date']);
		});

		return $backups;
	}

	/**
	 * Delete a backup file
	 *
	 * @param string $filename Backup filename
	 * @return bool
	 */
	public function deleteBackup($filename)
	{
		global $conf;

		$file = $conf->kundenkarte->dir_output.'/backups/'.basename($filename);
		if (file_exists($file) && strpos($filename, 'kundenkarte_backup_') === 0) {
			return unlink($file);
		}
		return false;
	}

	/**
	 * Copy directory recursively
	 *
	 * @param string $src Source directory
	 * @param string $dst Destination directory
	 */
	private function copyDirectory($src, $dst)
	{
		if (!is_dir($dst)) {
			dol_mkdir($dst);
		}

		$dir = opendir($src);
		while (($file = readdir($dir)) !== false) {
			if ($file == '.' || $file == '..') {
				continue;
			}

			$srcFile = $src.'/'.$file;
			$dstFile = $dst.'/'.$file;

			if (is_dir($srcFile)) {
				$this->copyDirectory($srcFile, $dstFile);
			} else {
				copy($srcFile, $dstFile);
			}
		}
		closedir($dir);
	}

	/**
	 * Delete directory recursively
	 *
	 * @param string $dir Directory path
	 */
	private function deleteDirectory($dir)
	{
		if (!is_dir($dir)) {
			return;
		}

		$files = array_diff(scandir($dir), array('.', '..'));
		foreach ($files as $file) {
			$path = $dir.'/'.$file;
			if (is_dir($path)) {
				$this->deleteDirectory($path);
			} else {
				unlink($path);
			}
		}
		rmdir($dir);
	}

	/**
	 * Create ZIP archive from directory
	 *
	 * @param string $sourceDir Source directory
	 * @param string $zipFile Target ZIP file
	 * @return bool
	 */
	private function createZipArchive($sourceDir, $zipFile)
	{
		$zip = new ZipArchive();
		if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
			return false;
		}

		$sourceDir = realpath($sourceDir);
		$files = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator($sourceDir),
			RecursiveIteratorIterator::LEAVES_ONLY
		);

		foreach ($files as $file) {
			if (!$file->isDir()) {
				$filePath = $file->getRealPath();
				$relativePath = substr($filePath, strlen($sourceDir) + 1);
				$zip->addFile($filePath, $relativePath);
			}
		}

		return $zip->close();
	}

	/**
	 * Get backup statistics
	 *
	 * @return array Statistics array
	 */
	public function getStatistics()
	{
		global $conf;

		$stats = array(
			'total_anlagen' => 0,
			'total_files' => 0,
			'total_connections' => 0,
			'total_customers' => 0,
			'files_size' => 0,
		);

		// Count anlagen
		$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage WHERE entity = ".((int) $conf->entity);
		$resql = $this->db->query($sql);
		if ($resql && $obj = $this->db->fetch_object($resql)) {
			$stats['total_anlagen'] = $obj->cnt;
		}

		// Count files
		$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files WHERE entity = ".((int) $conf->entity);
		$resql = $this->db->query($sql);
		if ($resql && $obj = $this->db->fetch_object($resql)) {
			$stats['total_files'] = $obj->cnt;
		}

		// Count connections
		$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_connection WHERE entity = ".((int) $conf->entity);
		$resql = $this->db->query($sql);
		if ($resql && $obj = $this->db->fetch_object($resql)) {
			$stats['total_connections'] = $obj->cnt;
		}

		// Count customers with anlagen
		$sql = "SELECT COUNT(DISTINCT fk_soc) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage WHERE entity = ".((int) $conf->entity);
		$resql = $this->db->query($sql);
		if ($resql && $obj = $this->db->fetch_object($resql)) {
			$stats['total_customers'] = $obj->cnt;
		}

		// Calculate files size
		$filesDir = $conf->kundenkarte->dir_output.'/anlagen';
		if (is_dir($filesDir)) {
			$stats['files_size'] = $this->getDirectorySize($filesDir);
		}

		return $stats;
	}

	/**
	 * Get directory size recursively
	 *
	 * @param string $dir Directory path
	 * @return int Size in bytes
	 */
	private function getDirectorySize($dir)
	{
		$size = 0;
		foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)) as $file) {
			if ($file->isFile()) {
				$size += $file->getSize();
			}
		}
		return $size;
	}
}
