# -*- coding: utf-8 -*-
from qgis.PyQt.QtCore import QThread, pyqtSignal
from dataclasses import dataclass
import os
import requests
import zipfile
import logging
from urllib.parse import quote
from .i18n import TR

try:
    import certifi
    DEFAULT_CA = certifi.where()
except ImportError:
    DEFAULT_CA = True  # fallback to system store

# Set up logger (QGIS safe)
logger = logging.getLogger(__name__)
if not logger.handlers:
    handler = logging.StreamHandler()
    formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    logger.setLevel(logging.INFO)


@dataclass
class DownloadJob:
    url: str
    zip_fullpath: str
    target_fullpath: str

class DownloadThread(QThread):
    message = pyqtSignal(str)
    progress = pyqtSignal(int)
    success = pyqtSignal(str)
    error = pyqtSignal(str)
    finished = pyqtSignal()

    job_started = pyqtSignal(str)
    job_finished = pyqtSignal(str)

    def __init__(self):
        super().__init__()

        self._is_running = True

        self.proxy_server = None
        self.proxy_user = None
        self.proxy_password = None

        self.status_message = ""
        self.certificate = True

        self.jobs = []
        self.current = 0
        self.total = 0

    def addJob(self, url, zip_fullpath, target_fullpath=None):
        if url == "" or zip_fullpath == "":
            return
        self.jobs.append(
            DownloadJob(
                url=url,
                zip_fullpath=zip_fullpath,
                target_fullpath=target_fullpath
            )
        )

    def clearJobs(self):
        self.jobs.clear()
        self.current = 0

    def hasJobs(self):
        return self.current < len(self.jobs)

    def setProxyServer(self, proxy_server):
        if proxy_server and len(proxy_server) > 10:
            self.proxy_server = proxy_server.strip()
        else:
            self.proxy_server = None

    def getProxyServer(self):
        _proxy_server = self._get_proxies()
        if not _proxy_server:
            return "No proxy set."
        else:
            return _proxy_server["http"]

    def setProxyUser(self, proxy_user):
        self.proxy_user = proxy_user.strip()

    def setProxyPassword(self, proxy_password):
        self.proxy_password = proxy_password.strip()

    def setUrl(self, url):
        self.url = url.strip()

    def setCertificate(self, certificate):
        # certificate can be bool or path
        self.certificate = certificate if certificate else DEFAULT_CA

    def setFilePath(self, zip_fullpath):
        self.zip_fullpath = zip_fullpath.strip()

    def setStatus(self, status_message):
        self.status_message = status_message
        logger.info(status_message)

    def getStatus(self):
        return self.status_message

    def download_wo_thread(self):
        """Simple blocking download (no signals)."""
        if not self.url or not self.zip_fullpath:
            self.setStatus("URL or file path not set.")
            return False

        proxies = self._get_proxies()
        try:
            with requests.get(
                self.url,
                stream=True,
                proxies=proxies,
                verify=self.certificate,
                timeout=30,
            ) as r:
                r.raise_for_status()
                os.makedirs(os.path.dirname(self.zip_fullpath), exist_ok=True)
                with open(self.zip_fullpath, "wb") as f:
                    for chunk in r.iter_content(chunk_size=8192):
                        if chunk:
                            f.write(chunk)
            self.checkStatus()
            return True
        except Exception as e:
            self.setStatus(f"Download failed: {e}")
            return False

    def run(self):
        self.total = len(self.jobs)
        self._is_running = True

        while self.hasJobs() and self._is_running:
            job = self.jobs[self.current]

            self.message.emit(
                f"({self.current + 1}/{self.total}) "
                f"{os.path.basename(job.zip_fullpath)} ..."
            )

            if os.path.exists(job.zip_fullpath):
                self.message.emit(
                    TR.FILE_EXISTS(os.path.basename(job.zip_fullpath))
                )
                self.current += 1
                continue
            elif os.path.exists(job.target_fullpath):
                self.message.emit(
                    TR.FILE_EXISTS(os.path.basename(job.target_fullpath))
                )
                self.current += 1
                continue

            self.job_started.emit(job.zip_fullpath)

            ok = self._download_one(job)

            if ok:
                self.success.emit(job.zip_fullpath)
            else:
                self.error.emit(job.zip_fullpath)
                break

            self.job_finished.emit(job.zip_fullpath)

            self.current += 1

        self.finished.emit()

    def _download_one(self, job):
        proxies = self._get_proxies()

        try:
            self.setStatus(f"Downloading {os.path.basename(job.zip_fullpath)}")

            with requests.get(
                job.url,
                stream=True,
                proxies=proxies,
                verify=self.certificate,
                timeout=30,
            ) as r:
                r.raise_for_status()

                total_length = r.headers.get("content-length")
                os.makedirs(os.path.dirname(job.zip_fullpath), exist_ok=True)

                with open(job.zip_fullpath, "wb") as f:
                    total_length = int(total_length) if total_length is not None else None
                    dl = 0

                    for data in r.iter_content(chunk_size=4096):
                        if not self._is_running:
                            return False

                        if data:
                            dl += len(data)
                            f.write(data)
                            if total_length is not None and total_length > 0:
                                self.progress.emit(int(100 * dl / total_length))

            # Checks if valid zip
            if not os.path.exists(job.zip_fullpath):
                self.setStatus("Download failed: File does not exist.")
                return False

            # 1. Checks filesize
            if os.path.getsize(job.zip_fullpath) < 22:
                self.setStatus("Download failed: File is empty or too small to be a ZIP.")
                self._safe_remove(job.zip_fullpath)
                return False

            # 2. Checks ZIP file structure
            if not zipfile.is_zipfile(job.zip_fullpath):
                self.setStatus("Download failed: Server returned an invalid ZIP file (likely an error page).")
                self._safe_remove(job.zip_fullpath)
                return False

            # 3. Checks any other
            try:
                with zipfile.ZipFile(job.zip_fullpath, 'r') as zf:
                    # testzip() returns the filename if broken
                    bad_file = zf.testzip()
                    if bad_file is not None:
                        self.setStatus(f"Download failed: Corrupted file inside ZIP: {bad_file}")
                        self._safe_remove(job.zip_fullpath)
                        return False
            except Exception as e:
                self.setStatus(f"Download failed: Failed to open ZIP file: {e}")
                self._safe_remove(job.zip_fullpath)
                return False

            self.setStatus(f"Downloaded {os.path.basename(job.zip_fullpath)}")
            return True

        except requests.exceptions.RequestException as e:
            self.setStatus(str(e))
            self._safe_remove(job.zip_fullpath)
            return False

    def _safe_remove(self, path):
        if path and os.path.exists(path):
            try:
                os.remove(path)
            except OSError:
                pass


    def stop(self):
        self._is_running = False

    def remove(self):
        if self.zip_fullpath and os.path.exists(self.zip_fullpath):
            os.remove(self.zip_fullpath)

    def checkStatus(self):
        """Check if downloaded file is valid ZIP."""
        if not self.zip_fullpath:
            self.setStatus("The zipfile has not been defined.")
            return
        if os.path.exists(self.zip_fullpath):
            if os.stat(self.zip_fullpath).st_size == 0:
                self.setStatus(
                    "The zipfile exists but the filesize is zero: " + self.zip_fullpath
                )
            else:
                try:
                    ret = zipfile.ZipFile(self.zip_fullpath).testzip()
                    if ret is not None:
                        self.setStatus(
                            "The zipfile exists with a problem: " + self.zip_fullpath
                        )
                    else:
                        self.setStatus(
                            "The zipfile exists and is valid: " + self.zip_fullpath
                        )
                except Exception:
                    self.setStatus(
                        "The zipfile exists but may be corrupt: " + self.zip_fullpath
                    )
        else:
            self.setStatus("The zipfile does not exist: " + self.zip_fullpath)

    def _get_proxies(self):
        """Build proxy dict for requests."""
        proxies = None
        if not self.proxy_server:
            return proxies

        proxy_string = ""
        if self.proxy_user:
            proxy_string = quote(self.proxy_user)
            if self.proxy_password:
                proxy_string += ":" + quote(self.proxy_password)
            proxy_string += "@"

        if self.proxy_server.lower().startswith("https://"):
            proxy_host = self.proxy_server.replace("https://", "")
        elif self.proxy_server.lower().startswith("http://"):
            proxy_host = self.proxy_server.replace("http://", "")
        else:
            proxy_host = self.proxy_server

        proxies = {
            "http": f"http://{proxy_string}{proxy_host}",
            "https": f"http://{proxy_string}{proxy_host}",
        }
        return proxies
