<?php
/**
 * FileHandler - Xử lý file cho backup/restore
 *
 * Định dạng container .foxblock (custom binary):
 * - 8 bytes magic: "FOXBLOCK"
 * - 4 bytes version: "\x00\x00\x00\x01"
 * - Sau đó là các blocks nối tiếp:
 *   [4 bytes: header_len][header JSON][file_data (header_len bytes chứa size)]
 *
 * Mỗi block header JSON:
 * { "path": "relative/path", "size": 1234, "type": "file|sql|meta" }
 */

if (!defined('ABSPATH')) {
    exit;
}

class Foxblock_Backup_FileHandler
{
    const MAGIC        = 'FOXBLOCK';
    const VERSION      = "\x00\x00\x00\x01";
    const FILES_CHUNK  = 5000; // files per AJAX request

    // Thư mục cần backup (relative to WP_CONTENT_DIR)
    const BACKUP_DIRS = ['uploads', 'plugins', 'themes', 'languages'];

    // Thư mục/patterns cần bỏ qua
    const SKIP_DIRS = [
        'foxblock_backup',
        'foxblock_temp',
        'cache',
        'updraft',
        'ai1wm-backups',
        'backups',
        'backup',
        'tmp',
    ];

    const SKIP_EXTENSIONS = ['log', 'tmp', 'cache'];

    /**
     * Quét toàn bộ file cần backup, trả về danh sách
     * [['abs' => ..., 'rel' => ...], ...]
     */
    public static function scan_files()
    {
        $file_list = [];
        $wc_dir    = WP_CONTENT_DIR;

        foreach (self::BACKUP_DIRS as $sub) {
            $dir = $wc_dir . '/' . $sub;
            if (!is_dir($dir)) continue;

            $iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
                RecursiveIteratorIterator::SELF_FIRST
            );

            foreach ($iterator as $file) {
                if (!$file->isFile()) continue;

                $abs = wp_normalize_path($file->getPathname());
                $rel = 'wp-content/' . $sub . '/' . ltrim(
                    str_replace(wp_normalize_path($dir), '', $abs),
                    '/'
                );

                // Bỏ qua thư mục skip
                $skip = false;
                foreach (self::SKIP_DIRS as $sd) {
                    if (strpos($abs, '/' . $sd . '/') !== false) {
                        $skip = true;
                        break;
                    }
                }
                if ($skip) continue;

                // Bỏ qua extension
                $ext = strtolower(pathinfo($abs, PATHINFO_EXTENSION));
                if (in_array($ext, self::SKIP_EXTENSIONS)) continue;

                $file_list[] = [
                    'abs'  => $abs,
                    'rel'  => $rel,
                    'size' => $file->getSize(),
                ];
            }
        }

        return $file_list;
    }

    /**
     * Ghi file archive header (magic + version)
     */
    public static function write_archive_header($fh)
    {
        fwrite($fh, self::MAGIC);
        fwrite($fh, self::VERSION);
    }

    /**
     * Ghi 1 block vào archive (header JSON + data)
     * Hỗ trợ stream: đọc file nguồn theo chunks
     */
    public static function write_block($fh_archive, $header, $source_path_or_string, $is_string = false)
    {
        $header_json = json_encode($header, JSON_UNESCAPED_UNICODE);
        $header_len  = strlen($header_json);

        // Ghi header length (4 bytes, big-endian)
        fwrite($fh_archive, pack('N', $header_len));
        // Ghi header JSON
        fwrite($fh_archive, $header_json);

        if ($is_string) {
            fwrite($fh_archive, $source_path_or_string);
        } else {
            // Stream file data
            $fh_src = fopen($source_path_or_string, 'rb');
            if ($fh_src) {
                while (!feof($fh_src)) {
                    fwrite($fh_archive, fread($fh_src, 65536)); // 64KB chunks
                }
                fclose($fh_src);
            }
        }
    }

    /**
     * Đọc TOC (table of contents) từ archive để lấy danh sách blocks
     * Trả về mảng [['header' => [...], 'data_offset' => int], ...]
     */
    public static function read_archive_toc($archive_path)
    {
        $fh  = fopen($archive_path, 'rb');
        if (!$fh) return ['toc' => [], 'error' => 'Không thể mở file archive'];

        // Đọc magic
        $magic = fread($fh, 8);
        if ($magic !== self::MAGIC) {
            fclose($fh);
            return ['toc' => [], 'error' => 'File không đúng định dạng Foxblock'];
        }

        // Bỏ qua version (4 bytes)
        fread($fh, 4);

        $toc    = [];
        $offset = 12; // sau magic(8) + version(4)

        while (!feof($fh)) {
            // Đọc header length (4 bytes)
            $raw_len = fread($fh, 4);
            if (strlen($raw_len) < 4) break;

            $arr        = unpack('N', $raw_len);
            $header_len = $arr[1];
            if ($header_len <= 0 || $header_len > 65536) break;

            // Đọc header JSON
            $header_json = fread($fh, $header_len);
            $header      = json_decode($header_json, true);
            if (!$header || !isset($header['size'])) break;

            $data_offset = $offset + 4 + $header_len;

            $toc[] = [
                'header'      => $header,
                'data_offset' => $data_offset,
            ];

            // Nhảy qua data
            $offset = $data_offset + $header['size'];
            fseek($fh, $offset);
        }

        fclose($fh);
        return ['toc' => $toc, 'error' => ''];
    }

    /**
     * Trích xuất 1 block từ archive và ghi ra file/string
     * Hỗ trợ giải nén từng phần (resume) để tránh timeout với file quá lớn.
     */
    public static function extract_block($archive_path, $data_offset, $size, $dest_path = null, $start_time = 0, $time_limit = 0, $resume_offset = 0)
    {
        $fh = fopen($archive_path, 'rb');
        if (!$fh) return false;

        if ($dest_path === null) {
            fseek($fh, $data_offset);
            // Đọc vào string toàn bộ một cách an toàn (tránh lỗi fread không tự đọc đủ $size)
            $content = '';
            $remaining = $size;
            while ($remaining > 0 && !feof($fh)) {
                $chunk = fread($fh, min(65536, $remaining));
                if ($chunk === false || strlen($chunk) === 0) break;
                $content .= $chunk;
                $remaining -= strlen($chunk);
            }
            fclose($fh);
            return $content;
        }

        // Nếu có dest_path -> ghi file ra đĩa
        wp_mkdir_p(dirname($dest_path));

        // Nếu có resume_offset, mở dạng append (ab), ngược lại write (wb)
        $mode = ($resume_offset > 0) ? 'ab' : 'wb';
        $fh_dest = fopen($dest_path, $mode);
        
        if (!$fh_dest) {
            fclose($fh);
            return false;
        }

        fseek($fh, $data_offset + $resume_offset);

        $remaining = $size - $resume_offset;
        $extracted_this_run = 0;

        while ($remaining > 0 && !feof($fh)) {
            $chunk     = min(65536, $remaining);
            $data      = fread($fh, $chunk);
            if ($data === false || strlen($data) === 0) break;
            
            fwrite($fh_dest, $data);
            $written = strlen($data);
            $remaining -= $written;
            $extracted_this_run += $written;

            // Kiểm tra timeout nếu được truyền vào
            if ($time_limit > 0 && $start_time > 0) {
                if ((microtime(true) - $start_time) >= $time_limit) {
                    fclose($fh_dest);
                    fclose($fh);
                    return [
                        'done' => false,
                        'extracted' => $resume_offset + $extracted_this_run
                    ];
                }
            }
        }

        fclose($fh_dest);
        fclose($fh);
        return ['done' => true];
    }

    /**
     * Lấy danh sách backup files từ thư mục backup
     */
    public static function list_backups()
    {
        require_once FOXBLOCK_PLUGIN_DIR . 'includes/Backup/SecurityHelper.php';
        $backup_dir = Foxblock_Backup_SecurityHelper::get_backup_dir();
        $backups    = [];

        if (is_dir($backup_dir)) {
            $files = glob($backup_dir . '/*.foxblock');
            if ($files) {
                foreach ($files as $file) {
                    $backups[] = [
                        'filename' => basename($file),
                        'size'     => filesize($file),
                        'size_fmt' => size_format(filesize($file)),
                        'date'     => date('d/m/Y H:i:s', filemtime($file)),
                        'timestamp'=> filemtime($file),
                        'path'     => $file,
                        'location' => 'Local'
                    ];
                }
            }
        }

        require_once FOXBLOCK_PLUGIN_DIR . 'includes/Backup/Google/class-google-drive-service.php';
        $gdrive_service = new Foxblock_Backup_Google_Drive_Service();
        if ($gdrive_service->is_ready()) {
            $folder_name = get_option('foxblock_gdrive_folder', 'Foxblock Backups');
            $gdrive_files = $gdrive_service->list_files($folder_name);
            if (!is_wp_error($gdrive_files) && is_array($gdrive_files)) {
                foreach ($gdrive_files as $gf) {
                    $found = false;
                    foreach ($backups as &$b) {
                        if ($b['filename'] === $gf['name']) {
                            $b['location'] = 'Local & GDrive';
                            $b['gdrive_id'] = $gf['id'];
                            $b['gdrive_link'] = $gf['webContentLink'] ?? '';
                            $found = true;
                            break;
                        }
                    }
                    if (!$found) {
                        $backups[] = [
                            'filename' => $gf['name'],
                            'size'     => $gf['size'] ?? 0,
                            'size_fmt' => isset($gf['size']) ? size_format($gf['size']) : 'N/A',
                            'date'     => date('d/m/Y H:i:s', strtotime($gf['createdTime'])),
                            'timestamp'=> strtotime($gf['createdTime']),
                            'path'     => '',
                            'location' => 'Google Drive',
                            'gdrive_id'=> $gf['id'],
                            'gdrive_link'=> $gf['webContentLink'] ?? ''
                        ];
                    }
                }
            }
        }

        usort($backups, function($a, $b) {
            return $b['timestamp'] - $a['timestamp'];
        });

        return $backups;
    }

    /**
     * Sao chép thư mục đệ quy (Dùng để clone Foxblock giữ mạng sống khi swap wp-content/plugins)
     */
    public static function copy_dir($src, $dst)
    {
        if (!is_dir($src)) return false;
        @mkdir($dst, 0777, true);
        $dir = opendir($src);
        if (!$dir) return false;
        
        while (false !== ($file = readdir($dir))) {
            if ($file !== '.' && $file !== '..') {
                $srcFile = $src . '/' . $file;
                $dstFile = $dst . '/' . $file;
                if (is_dir($srcFile)) {
                    self::copy_dir($srcFile, $dstFile);
                } else {
                    @copy($srcFile, $dstFile);
                }
            }
        }
        closedir($dir);
        return true;
    }

    /**
     * Xóa toàn bộ thư mục đệ quy (Dùng để dọn dẹp các bản build/backup rác)
     */
    public static function delete_dir($dir)
    {
        if (!is_dir($dir)) return false;
        $items = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST
        );
        foreach ($items as $item) {
            if ($item->isDir()) {
                @rmdir($item->getRealPath());
            } else {
                @unlink($item->getRealPath());
            }
        }
        return @rmdir($dir);
    }
}
