# -*- coding: utf-8 -*-

"""Utility functions for a2r

Author: G.J.J. van den Burg
License: See LICENSE file
Copyright: 2019, G.J.J. van den Burg

"""

import os
import string
import subprocess
import time

import requests
import unidecode

from pikepdf import Pdf
from pikepdf import PdfError

from .exceptions import BlockedByCloudFlareError
from .exceptions import FileTypeError
from .exceptions import NoPDFToolError
from .exceptions import RemarkableError
from .log import Logger

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) "
    "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 "
    "Safari/537.36"
}

HTTP_SERVICE_UNAVAILABLE = 503

REMARKABLE_USB_URL = "http://10.11.99.1"

logger = Logger()


def clean_string(s):
    """Clean a string by replacing accented characters with equivalents and
    keeping only the allowed characters (ascii letters, digits, underscore,
    space, dash, and period)"""
    normalized = unidecode.unidecode(s)
    allowed = string.ascii_letters + string.digits + "_ .-"
    cleaned = "".join(c if c in allowed else "_" for c in normalized)
    while "__" in cleaned:
        cleaned = cleaned.replace("__", "_")
    cleaned = cleaned.strip("_")
    return cleaned


def assert_file_is_pdf(filename):
    """Assert that a given file is a PDF file.

    This is done by trying to open it using pikepdf.
    """
    try:
        pdf = Pdf.open(filename)
        pdf.close()
        del pdf
        return True
    except PdfError:
        raise FileTypeError(filename, "pdf")


def download_url(url, filename, cookiejar=None):
    """Download the content of an url and save it to a filename"""
    logger.info("Downloading file at url: %s" % url)
    content = get_page_with_retry(url, cookiejar=cookiejar)
    with open(filename, "wb") as fid:
        fid.write(content)


def get_page_with_retry(url, tries=5, cookiejar=None, return_text=False):
    count = 0
    res = None
    jar = {} if cookiejar is None else cookiejar
    while count < tries:
        count += 1
        error = False
        try:
            res = requests.get(url, headers=HEADERS, cookies=jar)
        except requests.exceptions.ConnectionError:
            error = True

        if (
            res is not None
            and res.status_code == HTTP_SERVICE_UNAVAILABLE
            and res.headers.get("server", "") == "cloudflare"
        ):
            raise BlockedByCloudFlareError(url)

        if error or not res.ok:
            logger.warning(
                "(%i/%i) Error getting url %s. Retrying in 5 seconds."
                % (count, tries, url)
            )
            time.sleep(5)
            continue

        logger.info("Downloaded url: %s" % url)
        if return_text:
            return res.text
        return res.content


def get_content_type_with_retry(url, tries=5, cookiejar=None):
    if cookiejar is None:
        jar = requests.cookies.RequestsCookieJar()
    else:
        jar = cookiejar

    msg = "(%i/%i) Error getting content type for %s. Retrying in 5 seconds."

    # In rare cases, a HEAD request fails but a GET request does work. So here
    # we try both
    ops = [requests.head, requests.get]
    kwargs = dict(headers=HEADERS, cookies=jar, allow_redirects=True)
    for op in ops:
        count = 0
        while count < tries:
            count += 1
            error = False
            try:
                res = op(url, **kwargs)
            except requests.exceptions.ConnectionError:
                error = True
            if error or not res.ok:
                logger.warning(msg % (count, tries, url))
                time.sleep(5)
                continue
            return res.headers.get("Content-Type", None)
    return None


def follow_redirects(url):
    """Follow redirects from the URL (at most 100)"""
    it = 0
    jar = requests.cookies.RequestsCookieJar()
    while it < 100:
        req = requests.head(
            url, headers=HEADERS, allow_redirects=False, cookies=jar
        )
        if req.status_code == 200:
            break
        if not "Location" in req.headers:
            break
        new_url = req.headers["Location"]
        url = requests.compat.urljoin(url, new_url)
        jar.update(req.cookies)
        it += 1
    if it == 100:
        logger.warning("Max redirects reached. There may be a problem.")
    jar = jar or req.cookies
    return url, jar


def upload_to_remarkable_rmapi(
    filepath, remarkable_dir="/", rmapi_path="rmapi"
):
    logger.info("Starting upload to reMarkable")

    # Create the reMarkable dir if it doesn't exist
    remarkable_dir = remarkable_dir.rstrip("/")
    if remarkable_dir:
        parts = remarkable_dir.split("/")
        rmdir = ""
        while parts:
            rmdir += "/" + parts.pop(0)
            status = subprocess.call(
                [rmapi_path, "mkdir", rmdir],
                stdout=subprocess.DEVNULL,
            )
            if not status == 0:
                raise RemarkableError(
                    "Creating directory %s on reMarkable failed"
                    % remarkable_dir
                )

    # Upload the file
    status = subprocess.call(
        [rmapi_path, "put", filepath, remarkable_dir + "/"],
        stdout=subprocess.DEVNULL,
    )
    if not status == 0:
        raise RemarkableError(
            "Uploading file %s to reMarkable failed" % filepath
        )
    logger.info("Upload successful.")


def list_usb_dir(guid):
    url = REMARKABLE_USB_URL + "/documents/"
    if guid:
        url += guid + "/"
    response = requests.post(url)
    return response.json()


def crawl_usb_dirs(remarkable_dir):
    parts = [p for p in remarkable_dir.split("/") if p]
    guid = None
    while len(parts) > 0:
        part = parts.pop(0)
        data = list_usb_dir(guid)
        guid = None
        for item in data:
            if item["VissibleName"] == part:
                guid = item["ID"]
        if not guid:
            raise RemarkableError(
                "Directory %s does not exist on reMarkable" % remarkable_dir
            )
    # necessary because the last listed dir is the one uploaded to
    list_usb_dir(guid)


def upload_to_remarkable_usb(filepath, remarkable_dir="/"):
    # https://remarkable.guide/tech/usb-web-interface.html
    logger.info("Starting upload to reMarkable")

    # Check if the remarkable dir exists (no creation through usb api)
    crawl_usb_dirs(remarkable_dir)

    headers = {
        "Origin": REMARKABLE_USB_URL,
        "Accept": "*/*",
        "Referer": REMARKABLE_USB_URL + "/",
        "Connection": "keep-alive",
    }
    response = requests.post(
        REMARKABLE_USB_URL + "/upload",
        files={
            "file": (
                os.path.basename(filepath),
                open(filepath, "rb"),
                "application/pdf",
            )
        },
        headers=headers,
    )
    if not response.ok:
        raise RemarkableError(
            "Uploading file %s to reMarkable failed" % filepath
        )
    logger.info("Upload successful.")


def check_pdftool(pdftk_path, qpdf_path):
    """Check whether we have pdftk or qpdf available"""
    # set defaults in case either is set to None or something
    pdftk_path = pdftk_path or "false"
    qpdf_path = qpdf_path or "false"

    try:
        status = subprocess.call(
            [pdftk_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
        )
    except FileNotFoundError:
        status = 1
    if status == 0:
        return "pdftk"
    try:
        status = subprocess.call(
            [qpdf_path, "--help"],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
    except FileNotFoundError:
        status = 1
    if status == 0:
        return "qpdf"
    raise NoPDFToolError


class chdir:
    """Change directory in context and return to original on exit or failure"""

    def __init__(self, target: str):
        self._target = target
        self._original_dir = None

    def __enter__(self):
        self._original_dir = os.getcwd()
        os.chdir(self._target)

    def __exit__(self, exc_type, exc_value, traceback):
        os.chdir(self._original_dir)
