import logging
from typing import Dict, Optional
from uuid import uuid4

import pendulum
import requests
import trafilatura
from bs4 import BeautifulSoup
from django.apps import apps
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.files.base import ContentFile
from django.db import models
from django.urls import reverse
from django_extensions.db.models import TimeStampedModel
from htmldate import find_date
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
from scrobbles.tasks import push_scrobble_to_archivebox
from taggit.managers import TaggableManager

logger = logging.getLogger(__name__)
BNULL = {"blank": True, "null": True}
User = get_user_model()


class Domain(TimeStampedModel):
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    root = models.CharField(max_length=255)
    name = models.CharField(max_length=255, **BNULL)

    tags = TaggableManager(blank=True, verbose_name="Tags")

    def __str__(self) -> str:
        if self.name:
            return str(self.name)
        return str(self.root)

    def scrobbles_for_user(self, user_id):
        from scrobbles.models import Scrobble

        return Scrobble.objects.filter(web_page__domain=self, user_id=user_id).order_by(
            "-timestamp"
        )


def _fetch_url_raw(url: str) -> Optional[str]:
    """Fetch raw HTML for a URL.

    Tries two strategies in order:
      1. trafilatura (standard, works for most sites)
      2. cloudscraper (handles Cloudflare JS challenges)

    cloudscraper is lazy-imported so deployments that cannot compile
    its dependencies (e.g. FreeBSD) still work.
    """
    raw = trafilatura.fetch_url(url)
    if raw:
        return raw

    logger.debug("trafilatura returned nothing for %s, trying cloudscraper", url)
    try:
        import cloudscraper
    except ImportError:
        logger.debug("cloudscraper not available")
    else:
        try:
            scraper = cloudscraper.create_scraper()
            resp = scraper.get(url, timeout=30)
            resp.raise_for_status()
            return resp.text
        except Exception as exc:
            logger.debug("cloudscraper failed for %s: %s", url, exc)

    return None


class WebPage(ScrobblableMixin):
    COMPLETION_PERCENT = getattr(settings, "WEBSITE_COMPLETION_PERCENT", 100)

    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    url = models.URLField(max_length=500)
    date = models.DateField(**BNULL)
    domain = models.ForeignKey(Domain, on_delete=models.DO_NOTHING, **BNULL)
    extract = models.TextField(**BNULL)

    image = models.ImageField(upload_to="webpage/", **BNULL)
    image_small = ImageSpecField(
        source="image",
        processors=[ResizeToFit(100, 100)],
        format="JPEG",
        options={"quality": 60},
    )
    image_medium = ImageSpecField(
        source="image",
        processors=[ResizeToFit(300, 300)],
        format="JPEG",
        options={"quality": 75},
    )

    def __str__(self) -> str:
        if self.title:
            return "{} ({})".format(self.title, self.domain)
        if self.domain:
            return "Unknown ({})".format(self.domain)
        return str(self.uuid)

    def _raw_domain(self):
        self.url.split("//")[-1].split("/")[0]

    def _update_extract_from_web(self, raw_text: str = "", force=True):
        if not raw_text:
            raw_text = _fetch_url_raw(self.url)
        if not self.extract or force:
            self.extract = trafilatura.extract(raw_text)
            self.save(update_fields=["extract"])

    def get_absolute_url(self):
        return reverse("webpages:webpage_detail", kwargs={"slug": self.uuid})

    def get_read_url(self):
        return reverse("webpages:webpage_read", kwargs={"slug": self.uuid})

    @property
    def estimated_time_to_read_in_seconds(self):
        if not self.extract:
            return 600

        words_per_minute = getattr(settings, "READING_WORDS_PER_MINUTE", 200)
        words = len(self.extract.split(" "))
        return int(words / words_per_minute) * 60

    @property
    def estimated_time_to_read_in_minutes(self):
        return int(self.estimated_time_to_read_in_seconds / 60)

    @property
    def strings(self) -> ScrobblableConstants:
        return ScrobblableConstants(verb="Browsing", tags="earth_americas")

    @property
    def subtitle(self):
        return self.domain

    @property
    def primary_image_url(self) -> str:
        if self.image:
            return self.image.url
        return ""

    def scrobble_for_user(self, user_id):
        Scrobble = apps.get_model("scrobbles", "Scrobble")
        scrobble_data = self.basic_scrobble_data(user_id)
        logger.info(
            "[scrobble_for_user] called for webpage",
            extra={
                "webpage_id": self.id,
                "user_id": user_id,
                "scrobble_data": scrobble_data,
                "media_type": Scrobble.MediaType.WEBPAGE,
            },
        )
        scrobble = Scrobble.create_or_update(self, user_id, scrobble_data)
        push_scrobble_to_archivebox.delay(scrobble.id)
        return scrobble

    def scrobbles(self, user):
        Scrobble = apps.get_model("scrobbles", "Scrobble")
        return Scrobble.objects.filter(user=user, web_page=self).order_by("-timestamp")

    def clean_title(self, title: str, save=True):
        if len(title.split("|")) > 1:
            title = title.split("|")[0]
        if len(title.split("&#8211;")) > 1:
            title = title.split("&#8211;")[0]
        if len(title.split(" - ")) > 1:
            title = title.split(" - ")[0]
        self.title = title.strip()

        if save:
            self.save(update_fields=["title"])

    def _update_domain_from_url(self, save=False):
        domain = self.url.split("//")[-1].split("/")[0].split("www.")[-1]
        self.domain, created = Domain.objects.get_or_create(root=domain)

        if save:
            self.save(update_fields=["domain"])

    def _update_title_from_web(self, raw_text: str, save=False):
        if not raw_text:
            return

        soup = BeautifulSoup(raw_text, "html.parser")
        title_tag = soup.find("title")
        if title_tag and title_tag.string:
            self.title = title_tag.string
        elif not self.title and self.extract:
            first_line = self.extract.split("\n")[0]
            self.title = first_line[:254]

        if save:
            self.save(update_fields=["title"])

    def _update_date_from_web(self, save=False):
        try:
            date_str = find_date(str(self.url))
        except ValueError:
            date_str = ""
        if date_str:
            self.date = pendulum.parse(date_str).date()

        if save:
            self.save(update_fields=["date"])

    def push_to_archivebox(self, user):
        profile = user.profile
        url = profile.archivebox_url
        if not url:
            return
        username = profile.archivebox_username
        password = profile.archivebox_password
        login_url = requests.compat.urljoin(url, "admin/login/")
        session = requests.Session()
        response = session.get(login_url)
        csrf_token = response.cookies.get_dict().get("csrftoken")
        response = session.post(
            login_url,
            data={
                "username": username,
                "password": password,
                "csrfmiddlewaretoken": csrf_token,
            },
        )
        try:
            response = session.post(
                requests.compat.urljoin(url, "add/"),
                data={
                    "url": self.url + "\n",
                    "tags": "vrobbler",
                    "depth": "0",
                    "parser": "auto",
                },
                timeout=2,
            )
        except requests.exceptions.ReadTimeout:
            return

        if response.status_code == 200:
            logger.info("Website already exists in archive", extra={"url": self.url})
        else:
            raise Exception(
                f"Failed to push URL to archivebox (Response {response.status_code})"
            )

    def _extract_image_url_from_html(self, raw_html: str) -> Optional[str]:
        """Extract og:image or twitter:image from HTML."""
        if not raw_html:
            return None

        soup = BeautifulSoup(raw_html, "html.parser")

        og_image = soup.find("meta", property="og:image")
        if og_image and og_image.get("content"):
            return og_image["content"]

        twitter_image = soup.find("meta", attrs={"name": "twitter:image"})
        if twitter_image and twitter_image.get("content"):
            return twitter_image["content"]

        return None

    def _download_and_save_image(self, image_url: str) -> bool:
        """Download and save the webpage image."""
        if not image_url:
            return False

        try:
            response = requests.get(image_url, timeout=10)
            response.raise_for_status()

            filename = image_url.split("/")[-1].split("?")[0]
            if not filename.endswith((".jpg", ".jpeg", ".png", ".webp")):
                filename = "webpage_image.jpg"

            self.image.save(filename, ContentFile(response.content), save=True)
            return True
        except Exception:
            return False

    def fetch_data_from_web(self, save=True, force=True):
        raw_text = _fetch_url_raw(self.url)
        if not self.extract or force:
            self.extract = trafilatura.extract(
                raw_text,
                include_links=False,
                include_comments=False,
            )

        if not self.title or force:
            self._update_title_from_web(raw_text)

        if not self.date or force:
            self._update_date_from_web()

        if not self.domain or force:
            self._update_domain_from_url()

        if not self.base_run_time_seconds or force:
            self.base_run_time_seconds = self.estimated_time_to_read_in_seconds

        if (not self.image or force) and raw_text:
            image_url = self._extract_image_url_from_html(raw_text)
            if image_url:
                self._download_and_save_image(image_url)

        if save:
            self.save()

    @classmethod
    def find_or_create(cls, data_dict: Dict) -> "WebPage":
        """Given a data dict from an manual URL scrobble, does the heavy lifting of looking up
        the url, creating if if doesn't exist yet.

        """
        # TODO Add constants for all these data keys
        if "url" not in data_dict.keys():
            logger.error("No url in data dict")
            return

        webpage = cls.objects.filter(url=data_dict.get("url")).first()

        if not webpage:
            webpage = cls(url=data_dict.get("url"))
            webpage.fetch_data_from_web(save=True)
        else:
            webpage._archive_and_refetch()
        return webpage

    def _archive_and_refetch(self):
        """Archive current content to HistoricalWebPage and re-fetch from web."""
        if self.extract or self.date or self.domain:
            HistoricalWebPage.objects.create(
                webpage=self,
                date=self.date,
                domain=self.domain,
                extract=self.extract,
            )

        self.extract = None
        self.date = None
        self.domain = None
        self.title = None
        self.base_run_time_seconds = None
        self.image = None
        self.fetch_data_from_web(save=True, force=True)


class HistoricalWebPage(TimeStampedModel):
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    webpage = models.ForeignKey(
        WebPage, on_delete=models.CASCADE, related_name="historical_webpages"
    )
    date = models.DateField(**BNULL)
    domain = models.ForeignKey(Domain, on_delete=models.DO_NOTHING, **BNULL)
    extract = models.TextField(**BNULL)

    def __str__(self) -> str:
        if self.webpage.title:
            return "{} ({}) - {}".format(
                self.webpage.title, self.webpage.domain, self.created
            )
        return "{} - {}".format(self.webpage.url, self.created)
