# -*- coding: utf-8 -*-
"""
LVT4U File Utilities — OneDrive safety and GPKG WAL checkpoint.

Patterns adapted from QFieldSync (OPENGIS.ch) for Windows OneDrive compatibility.
Author: Lộc Vũ Trung (LVT) / Slow Forest
"""

import os
import time
import sqlite3
import hashlib
import shutil
import platform
from qgis.core import QgsMessageLog, Qgis

TAG = 'LVT4U'

# OneDrive Files On-Demand file attributes (Windows)
_FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS = 0x00400000
_FILE_ATTRIBUTE_RECALL_ON_OPEN = 0x00040000


def gpkg_wal_checkpoint(gpkg_path: str) -> bool:
    """Flush GPKG WAL journal to main database before copy/upload.

    GPKG uses SQLite WAL mode. Without checkpoint, copied files
    may be missing the most recent uncommitted data.
    Returns True if successful.
    """
    try:
        wal_path = gpkg_path + '-wal'
        if not os.path.exists(wal_path):
            return True  # No WAL file, nothing to flush

        wal_size = os.path.getsize(wal_path)
        if wal_size == 0:
            return True  # Empty WAL

        conn = sqlite3.connect(gpkg_path)
        try:
            conn.execute('PRAGMA wal_checkpoint(TRUNCATE)')
            QgsMessageLog.logMessage(
                f'WAL checkpoint: {os.path.basename(gpkg_path)} '
                f'(flushed {wal_size} bytes)', TAG, Qgis.MessageLevel.Info)
        finally:
            conn.close()
        return True
    except Exception as e:
        QgsMessageLog.logMessage(
            f'WAL checkpoint failed for {gpkg_path}: {e}', TAG, Qgis.MessageLevel.Warning)
        return False


def is_onedrive_cloud_file(path: str) -> bool:
    """Check if a file is an OneDrive placeholder (dehydrated/cloud-only).

    On Windows, OneDrive can show files that aren't actually downloaded.
    Attempting to read them will trigger a download or raise errors.
    """
    if platform.system() != 'Windows':
        return False
    try:
        import ctypes  # noqa: PLC0415
        attrs = ctypes.windll.kernel32.GetFileAttributesW(str(path))
        if attrs == -1:  # INVALID_FILE_ATTRIBUTES
            return False
        return bool(attrs & (_FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | _FILE_ATTRIBUTE_RECALL_ON_OPEN))
    except Exception:
        return False


def open_with_retry(path: str, mode: str = 'r', max_retries: int = 3,
                    initial_delay: float = 0.5, **kwargs):
    """Open a file with retry logic for OneDrive/cloud-locked files.

    Uses exponential backoff: 0.5s, 1s, 2s
    """
    delay = initial_delay
    last_error = None

    for attempt in range(max_retries + 1):
        try:
            return open(path, mode, **kwargs)
        except (PermissionError, OSError) as e:
            last_error = e
            if attempt < max_retries:
                QgsMessageLog.logMessage(
                    f'File locked, retry {attempt + 1}/{max_retries}: '
                    f'{os.path.basename(path)} ({e})', TAG, Qgis.MessageLevel.Warning)
                time.sleep(delay)
                delay *= 2
            else:
                raise
    raise last_error


def safe_copy_file(src: str, dst: str, max_retries: int = 3) -> bool:
    """Copy a file safely with OneDrive retry and GPKG WAL checkpoint.

    If the source is a .gpkg file, performs WAL checkpoint first.
    Uses retry logic for OneDrive-locked files.
    """
    # WAL checkpoint for GPKG files
    if src.lower().endswith('.gpkg'):
        gpkg_wal_checkpoint(src)

    delay = 0.5
    for attempt in range(max_retries + 1):
        try:
            # Copy main file
            shutil.copy2(src, dst)
            # Also copy WAL and SHM companion files for GPKG
            for ext in ['-wal', '-shm']:
                companion = src + ext
                if os.path.exists(companion) and os.path.getsize(companion) > 0:
                    shutil.copy2(companion, dst + ext)
            return True
        except (PermissionError, OSError) as e:
            if attempt < max_retries:
                QgsMessageLog.logMessage(
                    f'Copy retry {attempt + 1}/{max_retries}: '
                    f'{os.path.basename(src)} ({e})', TAG, Qgis.MessageLevel.Warning)
                time.sleep(delay)
                delay *= 2
            else:
                raise
    return False


def safe_remove_tree(path: str, max_retries: int = 3) -> bool:
    """Remove directory tree with OneDrive retry logic."""
    import stat

    def _onerror(func, fpath, _exc_info):
        """Error handler: clear read-only flag and retry."""
        os.chmod(fpath, stat.S_IWRITE)
        func(fpath)

    delay = 0.5
    for attempt in range(max_retries + 1):
        try:
            shutil.rmtree(path, onerror=_onerror)
            return True
        except (PermissionError, OSError) as e:
            if attempt < max_retries:
                time.sleep(delay)
                delay *= 2
            else:
                QgsMessageLog.logMessage(
                    f'Cannot remove {path}: {e}', TAG, Qgis.MessageLevel.Warning)
                return False
    return False


def compute_file_checksum(path: str) -> str:
    """Compute MD5 checksum of a file for change detection."""
    md5 = hashlib.md5(usedforsecurity=False)
    try:
        with open(path, 'rb') as f:
            for chunk in iter(lambda: f.read(8192), b''):
                md5.update(chunk)
        return md5.hexdigest()
    except Exception:
        return ''
