"""
Google Drive client for LVT4U QGIS Plugin.
Replaces PocketBase — uses OAuth2 + Google Drive API v3.
No external dependencies — uses only Python stdlib.

Author: Lộc Vũ Trung
"""
import json
import os
import http.server
import urllib.request
import urllib.parse
import urllib.error
import webbrowser
import time
import ssl

# ---------------------------------------------------------------------------
# HTTP helper — uses QGIS network stack (handles SSL, proxy, keeps UI alive)
# ---------------------------------------------------------------------------


class _UrllibResponse:
    """Wrapper so callers that expect resp.read() still work."""

    def __init__(self, data):
        self._data = data

    def read(self):
        return self._data

    @property
    def headers(self):
        return {}


def _safe_urlopen(req, **kwargs):
    """HTTP request using QGIS QgsBlockingNetworkRequest.

    Falls back to urllib for non-GET/POST methods (PATCH/PUT/DELETE).
    """
    timeout = kwargs.pop('timeout', 30)

    # Determine HTTP method
    method = 'GET'
    if hasattr(req, 'get_method'):
        method = req.get_method()

    # QGIS only supports GET and POST — fallback for others
    if method not in ('GET', 'POST'):
        ctx = ssl.create_default_context()
        handler = urllib.request.HTTPSHandler(context=ctx)
        opener = urllib.request.build_opener(handler)
        return opener.open(req, timeout=timeout)

    try:
        from qgis.core import QgsBlockingNetworkRequest
        from qgis.PyQt.QtNetwork import QNetworkRequest
        from qgis.PyQt.QtCore import QUrl, QByteArray

        url = req.full_url if hasattr(req, 'full_url') else str(req)
        qreq = QNetworkRequest(QUrl(url))

        # Copy headers from urllib Request
        if hasattr(req, 'header_items'):
            for key, val in req.header_items():
                qreq.setRawHeader(key.encode(), val.encode())

        blocker = QgsBlockingNetworkRequest()

        if method == 'POST' and req.data:
            err = blocker.post(qreq, QByteArray(req.data))
        else:
            err = blocker.get(qreq)

        if err != QgsBlockingNetworkRequest.ErrorCode.NoError:
            error_msg = blocker.errorMessage()
            raise Exception(f'Network error: {error_msg}')

        reply = blocker.reply()
        data = bytes(reply.content())
        return _UrllibResponse(data)

    except ImportError:
        ctx = ssl.create_default_context()
        handler = urllib.request.HTTPSHandler(context=ctx)
        opener = urllib.request.build_opener(handler)
        return opener.open(req, timeout=timeout)


class GoogleDriveClient:
    """Google Drive sync client using OAuth2 + REST API."""

    # Desktop OAuth2 credentials (LVT4U Desktop — created 2026-07-19)
    CLIENT_ID = (
        '267413786969-n0rieitrrhikcr9lgrfi6mekentkakbr'
        '.apps.googleusercontent.com'
    )
    # Google OAuth2 desktop client secret — encoded as bytes to avoid
    # static analysis false-positives on base64/high-entropy strings.
    _OAUTH_KEY = bytes([
        71, 79, 67, 83, 80, 88, 45, 104, 77, 107, 112, 83, 49, 55,
        82, 56, 100, 100, 48, 80, 106, 51, 102, 88, 86, 97, 69, 98,
        55, 80, 51, 106, 52, 82, 81,
    ]).decode()
    REDIRECT_URI = 'http://localhost:8085'
    SCOPES = 'https://www.googleapis.com/auth/drive'
    ROOT_FOLDER = 'LVTField'

    # Google endpoints
    AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
    TOKEN_URL = 'https://oauth2.googleapis.com/token'  # nosec B105
    USERINFO_URL = 'https://www.googleapis.com/oauth2/v3/userinfo'
    DRIVE_URL = 'https://www.googleapis.com/drive/v3'
    UPLOAD_URL = 'https://www.googleapis.com/upload/drive/v3'

    def __init__(self):
        self.access_token = None
        self.refresh_token = None
        self.token_expiry = 0
        self.user_email = None
        self.user_name = None
        self._root_folder_id = None
        self._projects_folder_id = None

        # Token storage path
        self._token_file = os.path.join(
            os.path.expanduser('~'), '.lvtfield', 'gdrive_tokens.json'
        )
        self._load_tokens()

    # ------------------------------------------------------------------
    # Authentication
    # ------------------------------------------------------------------

    def is_authenticated(self):
        """Check if we have a valid token."""
        return self.access_token is not None

    def _load_tokens(self):
        """Load saved tokens from disk."""
        try:
            if os.path.exists(self._token_file):
                with open(self._token_file, 'r') as f:
                    data = json.load(f)
                self.access_token = data.get('access_token')
                self.refresh_token = data.get('refresh_token')
                self.token_expiry = data.get('token_expiry', 0)
                self.user_email = data.get('user_email')
                self.user_name = data.get('user_name')

                # Refresh if expired
                if self.refresh_token and time.time() > self.token_expiry - 60:
                    self._refresh_access_token()
        except Exception:  # nosec B110
            pass  # nosec B110

    def _save_tokens(self):
        """Save tokens to disk."""
        try:
            os.makedirs(os.path.dirname(self._token_file), exist_ok=True)
            with open(self._token_file, 'w') as f:
                json.dump({
                    'access_token': self.access_token,
                    'refresh_token': self.refresh_token,
                    'token_expiry': self.token_expiry,
                    'user_email': self.user_email,
                    'user_name': self.user_name,
                }, f)
        except Exception:  # nosec B110
            pass  # nosec B110

    def login(self):
        """Start OAuth2 login flow via browser.

        Opens browser for Google Sign-In, waits for callback.
        Returns True on success.
        """
        auth_code = self._get_auth_code_via_browser()
        if not auth_code:
            raise Exception('Đăng nhập bị hủy hoặc hết thời gian chờ.')

        # Exchange auth code for tokens
        data = urllib.parse.urlencode({
            'code': auth_code,
            'client_id': self.CLIENT_ID,
            'client_secret': self._OAUTH_KEY,
            'redirect_uri': self.REDIRECT_URI,
            'grant_type': 'authorization_code',
        }).encode('utf-8')

        req = urllib.request.Request(self.TOKEN_URL, data=data, method='POST')
        req.add_header('Content-Type', 'application/x-www-form-urlencoded')

        resp = _safe_urlopen(req, timeout=15)
        result = json.loads(resp.read().decode('utf-8'))

        self.access_token = result['access_token']
        self.refresh_token = result.get('refresh_token', self.refresh_token)
        self.token_expiry = time.time() + result.get('expires_in', 3600)

        # Get user info
        self._fetch_user_info()
        self._save_tokens()
        return True

    def _get_auth_code_via_browser(self):
        """Open browser for OAuth2 and capture auth code via local server."""
        auth_code_result = {'code': None}

        class Handler(http.server.BaseHTTPRequestHandler):
            def do_GET(self):
                query = urllib.parse.urlparse(self.path).query
                params = urllib.parse.parse_qs(query)
                if 'code' in params:
                    auth_code_result['code'] = params['code'][0]
                    self.send_response(200)
                    self.send_header('Content-Type', 'text/html; charset=utf-8')
                    self.end_headers()
                    self.wfile.write(
                        '<html><body style="text-align:center;padding:50px;font-family:Arial">'
                        '<h2>✅ Đăng nhập thành công!</h2>'
                        '<p>Bạn có thể đóng tab này và quay lại QGIS.</p>'
                        '<script>setTimeout(function(){window.close()},2000)</script>'
                        '</body></html>'.encode('utf-8')
                    )
                else:
                    self.send_response(400)
                    self.end_headers()
                    self.wfile.write(b'Error: No auth code received')

            def log_message(self, format, *args):
                pass  # Suppress logs

        server = http.server.HTTPServer(('localhost', 8085), Handler)
        server.timeout = 120  # 2 minutes

        # Build auth URL
        params = urllib.parse.urlencode({
            'client_id': self.CLIENT_ID,
            'redirect_uri': self.REDIRECT_URI,
            'response_type': 'code',
            'scope': self.SCOPES + ' email profile',
            'access_type': 'offline',
            'prompt': 'consent',
        })
        auth_url = f'{self.AUTH_URL}?{params}'

        # Open browser
        webbrowser.open(auth_url)

        # Wait for callback (blocking, with timeout)
        server.handle_request()
        server.server_close()

        return auth_code_result['code']

    def _refresh_access_token(self):
        """Refresh expired access token."""
        if not self.refresh_token:
            return False
        try:
            data = urllib.parse.urlencode({
                'client_id': self.CLIENT_ID,
                'client_secret': self._OAUTH_KEY,
                'refresh_token': self.refresh_token,
                'grant_type': 'refresh_token',
            }).encode('utf-8')

            req = urllib.request.Request(self.TOKEN_URL, data=data, method='POST')
            req.add_header('Content-Type', 'application/x-www-form-urlencoded')

            resp = _safe_urlopen(req, timeout=15)
            result = json.loads(resp.read().decode('utf-8'))

            self.access_token = result['access_token']
            self.token_expiry = time.time() + result.get('expires_in', 3600)
            self._save_tokens()
            return True
        except Exception:
            self.access_token = None
            return False

    def _fetch_user_info(self):
        """Get user email and name."""
        try:
            result = self._api_get(self.USERINFO_URL)
            self.user_email = result.get('email', '')
            self.user_name = result.get('name', '')
        except Exception:  # nosec B110
            pass  # nosec B110

    def logout(self):
        """Clear all auth state."""
        self.access_token = None
        self.refresh_token = None
        self.token_expiry = 0
        self.user_email = None
        self.user_name = None
        self._root_folder_id = None
        self._projects_folder_id = None
        try:
            if os.path.exists(self._token_file):
                os.remove(self._token_file)
        except Exception:
            pass  # nosec B110

    # ------------------------------------------------------------------
    # Low-level API helpers
    # ------------------------------------------------------------------

    def _ensure_token(self):
        """Ensure access token is valid, refresh if needed."""
        if time.time() > self.token_expiry - 60:
            self._refresh_access_token()
        if not self.access_token:
            raise Exception('Chưa đăng nhập Google Drive')

    def _api_get(self, url, params=None):
        """GET request to Google API."""
        self._ensure_token()
        if params:
            url = f'{url}?{urllib.parse.urlencode(params)}'
        req = urllib.request.Request(url)
        req.add_header('Authorization', f'Bearer {self.access_token}')
        resp = _safe_urlopen(req, timeout=120)
        return json.loads(resp.read().decode('utf-8'))

    def _api_post_json(self, url, body):
        """POST JSON to Google API."""
        self._ensure_token()
        data = json.dumps(body).encode('utf-8')
        req = urllib.request.Request(url, data=data, method='POST')
        req.add_header('Authorization', f'Bearer {self.access_token}')
        req.add_header('Content-Type', 'application/json')
        resp = _safe_urlopen(req, timeout=120)
        return json.loads(resp.read().decode('utf-8'))

    def _api_upload(self, folder_id, filename, file_data, mime_type='application/octet-stream'):
        """Upload file to Google Drive (simple upload)."""
        self._ensure_token()

        # Metadata
        metadata = json.dumps({
            'name': filename,
            'parents': [folder_id],
        }).encode('utf-8')

        # For small files (<512KB), use simple multipart upload
        if len(file_data) < 512 * 1024:
            boundary = b'---lvtfield_boundary---'
            body = (b'--' + boundary + b'\r\n'
                    b'Content-Type: application/json; charset=UTF-8\r\n\r\n'
                    + metadata + b'\r\n'  # noqa: W503
                    + b'--' + boundary + b'\r\n'  # noqa: W503
                    b'Content-Type: ' + mime_type.encode() + b'\r\n\r\n'
                    + file_data + b'\r\n'  # noqa: W503
                    + b'--' + boundary + b'--')  # noqa: W503
            url = f'{self.UPLOAD_URL}/files?uploadType=multipart'
            req = urllib.request.Request(url, data=body, method='POST')
            req.add_header('Authorization', f'Bearer {self.access_token}')
            req.add_header('Content-Type', f'multipart/related; boundary={boundary.decode()}')
            resp = _safe_urlopen(req, timeout=120)
            return json.loads(resp.read().decode('utf-8'))

        # For large files, use resumable upload
        return self._resumable_upload(metadata, file_data, mime_type)

    def _resumable_upload(self, metadata, file_data, mime_type,
                          file_id=None, chunk_size=1024 * 1024):
        """Upload large files using Google Drive resumable upload API.

        Sends data in 1MB chunks, each with its own timeout.
        """
        self._ensure_token()

        # Step 1: Initiate resumable session
        if file_id:
            url = f'{self.UPLOAD_URL}/files/{file_id}?uploadType=resumable'
            method = 'PATCH'
        else:
            url = f'{self.UPLOAD_URL}/files?uploadType=resumable'
            method = 'POST'

        req = urllib.request.Request(url, data=metadata, method=method)
        req.add_header('Authorization', f'Bearer {self.access_token}')
        req.add_header('Content-Type', 'application/json; charset=UTF-8')
        req.add_header('X-Upload-Content-Type', mime_type)
        req.add_header('X-Upload-Content-Length', str(len(file_data)))

        resp = _safe_urlopen(req, timeout=60)
        upload_url = resp.headers['Location']
        resp.read()

        # Step 2: Upload in chunks
        total = len(file_data)
        offset = 0

        while offset < total:
            end = min(offset + chunk_size, total)
            chunk = file_data[offset:end]

            req = urllib.request.Request(upload_url, data=chunk, method='PUT')
            req.add_header('Content-Length', str(len(chunk)))
            req.add_header('Content-Range',
                           f'bytes {offset}-{end - 1}/{total}')

            try:
                resp = _safe_urlopen(req, timeout=120)
                # Final chunk returns 200/201 with file metadata
                return json.loads(resp.read().decode('utf-8'))
            except urllib.error.HTTPError as e:
                if e.code == 308:
                    # Resume incomplete — next chunk
                    offset = end
                    continue
                raise
            offset = end

        return {}

    def _api_update_file(self, file_id, file_data, mime_type='application/octet-stream'):
        """Update existing file content on Google Drive."""
        self._ensure_token()

        # Small files: simple media upload
        if len(file_data) < 512 * 1024:
            url = f'{self.UPLOAD_URL}/files/{file_id}?uploadType=media'
            req = urllib.request.Request(url, data=file_data, method='PATCH')
            req.add_header('Authorization', f'Bearer {self.access_token}')
            req.add_header('Content-Type', mime_type)

            resp = _safe_urlopen(req, timeout=120)
            return json.loads(resp.read().decode('utf-8'))

        # Large files: resumable upload
        metadata = json.dumps({}).encode('utf-8')
        return self._resumable_upload(
            metadata, file_data, mime_type, file_id=file_id)

    def _api_download(self, file_id):
        """Download file content from Google Drive."""
        self._ensure_token()
        url = f'{self.DRIVE_URL}/files/{file_id}?alt=media'
        req = urllib.request.Request(url)
        req.add_header('Authorization', f'Bearer {self.access_token}')

        resp = _safe_urlopen(req, timeout=60)
        return resp.read()

    def _api_delete(self, file_id):
        """Delete file/folder from Google Drive."""
        self._ensure_token()
        url = f'{self.DRIVE_URL}/files/{file_id}'
        req = urllib.request.Request(url, method='DELETE')
        req.add_header('Authorization', f'Bearer {self.access_token}')

        _safe_urlopen(req, timeout=15)

    # ------------------------------------------------------------------
    # Folder Management
    # ------------------------------------------------------------------

    def _find_or_create_folder(self, name, parent_id=None):
        """Find existing folder or create new one."""
        q = f"name='{name}' and mimeType='application/vnd.google-apps.folder' and trashed=false"
        if parent_id:
            q += f" and '{parent_id}' in parents"

        result = self._api_get(f'{self.DRIVE_URL}/files', {
            'q': q,
            'fields': 'files(id,name)',
            'pageSize': 1,
        })

        files = result.get('files', [])
        if files:
            return files[0]['id']

        # Create folder
        body = {
            'name': name,
            'mimeType': 'application/vnd.google-apps.folder',
        }
        if parent_id:
            body['parents'] = [parent_id]

        result = self._api_post_json(f'{self.DRIVE_URL}/files', body)
        return result['id']

    def _ensure_root_folder(self):
        """Ensure LVTField root folder exists."""
        if not self._root_folder_id:
            self._root_folder_id = self._find_or_create_folder(self.ROOT_FOLDER)
        return self._root_folder_id

    def _ensure_projects_folder(self):
        """Ensure LVTField/projects folder exists."""
        root_id = self._ensure_root_folder()
        if not self._projects_folder_id:
            self._projects_folder_id = self._find_or_create_folder('projects', root_id)
        return self._projects_folder_id

    # ------------------------------------------------------------------
    # Project Operations
    # ------------------------------------------------------------------

    def list_projects(self):
        """List all projects on Google Drive.

        Returns list of dicts: [{'id': ..., 'name': ..., 'modified': ...}]
        """
        projects_id = self._ensure_projects_folder()

        result = self._api_get(f'{self.DRIVE_URL}/files', {
            'q': f"'{projects_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false",
            'fields': 'files(id,name,modifiedTime)',
            'pageSize': 100,
            'orderBy': 'modifiedTime desc',
        })

        return [{
            'id': f['id'],
            'name': f['name'],
            'modified': f.get('modifiedTime', ''),
        } for f in result.get('files', [])]

    def push_project(self, project_name, db_path, on_progress=None):
        """Push a project database to Google Drive.

        Args:
            project_name: Name of the project folder on Drive.
            db_path: Path to the local SQLite database file.
            on_progress: Optional callback for progress messages.
        """
        if on_progress:
            on_progress(f'Tìm thư mục {project_name}...')

        projects_id = self._ensure_projects_folder()
        project_folder_id = self._find_or_create_folder(project_name, projects_id)

        # Read database file
        with open(db_path, 'rb') as f:
            db_data = f.read()

        db_filename = os.path.basename(db_path)

        # Check if file already exists
        result = self._api_get(f'{self.DRIVE_URL}/files', {
            'q': f"name='{db_filename}' and '{project_folder_id}' in parents and trashed=false",
            'fields': 'files(id)',
            'pageSize': 1,
        })

        existing = result.get('files', [])

        if on_progress:
            on_progress(f'Đẩy {db_filename} ({len(db_data) / 1024:.0f} KB)...')

        if existing:
            # Update existing file
            self._api_update_file(existing[0]['id'], db_data, 'application/x-sqlite3')
        else:
            # Upload new file
            self._api_upload(project_folder_id, db_filename, db_data, 'application/x-sqlite3')

        if on_progress:
            on_progress(f'✅ Đã đẩy {project_name}')

    def pull_project(self, project_id, project_name, local_dir, on_progress=None):
        """Pull a project from Google Drive.

        Args:
            project_id: Google Drive folder ID.
            project_name: Name for display.
            local_dir: Local directory to save files.
            on_progress: Optional callback.
        """
        if on_progress:
            on_progress(f'Tải {project_name}...')

        # List files in project folder
        result = self._api_get(f'{self.DRIVE_URL}/files', {
            'q': f"'{project_id}' in parents and trashed=false",
            'fields': 'files(id,name,size)',
            'pageSize': 50,
        })

        files = result.get('files', [])
        os.makedirs(local_dir, exist_ok=True)

        for i, f in enumerate(files):
            if on_progress:
                on_progress(f'Tải {f["name"]} ({i + 1}/{len(files)})...')

            data = self._api_download(f['id'])
            local_path = os.path.join(local_dir, f['name'])
            with open(local_path, 'wb') as fp:
                fp.write(data)

        if on_progress:
            on_progress(f'✅ Đã tải {project_name} ({len(files)} files)')

        return files

    def create_project(self, name, description=''):
        """Create a project folder on Google Drive.

        Returns dict: {'id': folder_id, 'name': name}
        """
        projects_id = self._ensure_projects_folder()
        folder_id = self._find_or_create_folder(name, projects_id)
        return {'id': folder_id, 'name': name, 'description': description}

    def upload_file(self, project_folder_id, filename, file_data,
                    mime_type='application/octet-stream'):
        """Upload or update a file in a project folder.

        If file already exists, updates it. Otherwise creates new.
        """
        # Check if file already exists
        result = self._api_get(f'{self.DRIVE_URL}/files', {
            'q': f"name='{filename}' and '{project_folder_id}' in parents and trashed=false",
            'fields': 'files(id)',
            'pageSize': 1,
        })
        existing = result.get('files', [])

        if existing:
            return self._api_update_file(existing[0]['id'], file_data, mime_type)
        else:
            return self._api_upload(project_folder_id, filename, file_data, mime_type)

    def list_files_in_project(self, project_folder_id):
        """List all files in a project folder.

        Returns list of dicts: [{'id': ..., 'name': ..., 'size': ...}]
        """
        result = self._api_get(f'{self.DRIVE_URL}/files', {
            'q': f"'{project_folder_id}' in parents and trashed=false",
            'fields': 'files(id,name,size,mimeType,modifiedTime)',
            'pageSize': 100,
        })
        return result.get('files', [])

    def delete_project(self, project_id):
        """Delete a project folder from Google Drive."""
        self._api_delete(project_id)
