#!/usr/bin/env python3

"""This is Articulo. 
Tiny library for extracting html article content."""

import re
from functools import cached_property
from typing import Union

import requests
from bs4 import BeautifulSoup, NavigableString, Tag
from requests import RequestException

from .exceptions import HTTPErrorException, MaxIterations, NoTitleException, NoHTMLException


class Articulo:
    """
    Articulo is the only and basic class of this library.
    Usage is really staightforward and simple: just import this class
    and instantiate with link as a parameter.
    """

    __max_iterations_count = 100

    def __init__(
        self,
        link: str,
        threshold: float = 0.7,
        verbose: bool = False,
        http_headers: Union[dict, None] = None,
    ) -> None:
        """
        Article object

        Parameters:
        @link: Link to article, that should be processed.
        @threshold (optional): Max information loss coefficient, that affects content parsing.
        @verbose (optional): Verbose mode. If enabled than all the operations will be logged.
        @http_headers: Additional headers for HTTP request. There is no default headers.
        """

        self.__link = link
        self.__threshold = threshold
        self.__verbose = verbose
        self.__http_headers = http_headers

    @property
    def title(self):
        """
        Parsed article title
        """
        return self.__title_element.text

    @property
    def text(self):
        """
        Parsed article main content text.
        """
        return self.__content_markup.text

    @property
    def markup(self):
        """
        Article main content html markup.
        """
        return str(self.__content_markup)

    @cached_property
    def description(self):
        """
        Article short description.
        """
        return self.__try_get_meta_content(
            ["name", "property"],
            ["description", "og:description", "twitter:description"],
        )

    @cached_property
    def preview(self):
        """
        Dict with article icons.
        Keys are sizes and values are links to icons.
        """
        return self.__try_get_meta_content(
            ["name", "property"], ["og:image", "twitter:image", "twitter:image:src"]
        )

    @cached_property
    def icon(self):
        """
        Link to article icon.
        The biggest possible icon will be returned if there is
        multiple icons and size attribute provided.
        In other case will be returned first icon.
        """
        icon_src = None
        last_biggest_size = 0

        soup = BeautifulSoup(self.__html, features="lxml")
        icons_meta = soup.findAll("link", attrs={"rel": "icon"})
        for icon in icons_meta:
            href: Union[str, None] = icon.get("href")
            size: Union[str, None] = icon.get("sizes")
            if size:
                [width, _] = [int(i) for i in size.split("x")]
                if width > last_biggest_size:
                    icon_src = href
                    last_biggest_size = width
            else:
                icon_src = href

        return icon_src

    @cached_property
    def keywords(self):
        """
        List of article's keywords.
        """
        kw_str = self.__try_get_meta_content(["name"], ["keywords"], '')
        return [] if len(kw_str) == 0 else [kw.strip() for kw in kw_str.split(",")]

    @cached_property
    def __content_markup(self):
        """
        Parses article HTML and returns the main article content markup using recursion.
        """
        soup = BeautifulSoup(self.__html, features="lxml")
        return self.__look_for_best_parent(soup.html, 0)

    @cached_property
    def __title_element(self):
        """
        Parses article html and returns article title.
        This method assumes, that article HTML has two things:
        * title tag - as an initial title
        * any tag at the body with matching content - as a reference point for the article content
        """

        soup = BeautifulSoup(self.__html, features="lxml")
        title = soup.find("title")

        if title is None:
            raise NoTitleException(self.__link)

        title_text = title.text
        title_meta = self.__try_find_meta(
            ["property", "name"], ["og:title", "twitter:title"]
        )

        print(title_meta)

        if not title_meta is None:
            title_text = title_meta.get("content")

        title_inner = soup.find(
            ["h1", "h2", "h3", "h4", "h5", "h6", "p"], string=re.compile(re.escape(title_text))
        )

        if title_inner is None:
            return title

        return title_inner

    @cached_property
    def __html(self) -> Union[str, None]:
        """
        Loads article html from link provided at the moment of an Articulo object instantiation.
        Returns full page html or None if request was not successful.
        """
        self.__log(f"Start loading article from {self.__link}...")
        response = requests.get(self.__link, timeout=2000, headers=self.__http_headers)
        try:
            response.raise_for_status()
        except RequestException as exc:
            self.__log("Error loading an article.")
            raise HTTPErrorException(
                f"Http error: {response.reason}", response.status_code
            ) from exc
        self.__log("Article loaded.")

        text = response.text

        if response.text is None or len(response.text) == 0:
            raise NoHTMLException(self.__link)

        return text

    def __try_find_meta(
        self, attr_keys: list[str], attr_values: list[str]
    ) -> Union[Tag, NavigableString, None]:
        """
        Looks for metatags content by their names
        """
        soup = BeautifulSoup(self.__html, features="lxml")
        for key in attr_keys:
            for val in attr_values:
                if soup.findAll("meta", attrs={key: val}):
                    return soup.find("meta", attrs={key: val})

        return None

    def __try_get_meta_content(
        self, attr_keys: list[str], attr_values: list[str], defval=None
    ):
        soup = self.__try_find_meta(attr_keys, attr_values)
        if soup is None:
            return defval

        return soup.get("content")

    def __look_for_best_parent(self, parent: Tag, iter_counter: int):
        """
        Recursively searches for the best parent element containing the main article content.
        """
        self.__log(f'Looking for an element containing "{self.__title_element.text}" title inside {parent.name.upper()} tag...') #pylint: disable=line-too-long

        if iter_counter >= self.__max_iterations_count:
            raise MaxIterations("Cannot find the best parent element within the maximum iterations.") #pylint: disable=line-too-long

        best_parent = None

        for child in parent.children:
            if isinstance(child, NavigableString):
                self.__log(f'Skipping the "{child}" string...')
                continue

            if child.find(self.__title_element.name, string=self.__title_element.text) is None:
                self.__log(f'Not found "{self.__title_element.text}" inside {child.name.upper()} tag. Skipping...') #pylint: disable=line-too-long
                continue

            self.__log(f'Found a {child.name.upper()} child tag with "{self.__title_element.text} inside."') #pylint: disable=line-too-long
            best_parent_content_length = len(parent.text)
            child_content_length = len(child.text)

            information_loss_coeff = 1.0 - (child_content_length / best_parent_content_length)

            if child == self.__title_element.parent and information_loss_coeff < self.__threshold:
                self.__log(f"Child element {child.name.upper()} is equal to title's parent element. Best possible parent is found.") #pylint: disable=line-too-long
                best_parent = child
                break

            if information_loss_coeff > self.__threshold:
                self.__log(f"Content loss coefficient: {information_loss_coeff}. The best possible parent is {parent.name.upper()}.") #pylint: disable=line-too-long
                best_parent = parent
                break


            iter_counter += 1

        if best_parent:
            return best_parent

        # Recursively search in child elements
        for child in parent.children:
            if isinstance(child, Tag):
                result = self.__look_for_best_parent(child, iter_counter)
                if result:
                    return result

        return None

    def __log(self, message: str) -> None:
        """
        Logs message if object instantiated with verbose mode.
        Params:
        @message: message to log
        """
        if self.__verbose:
            print(message)
