"""
Google Dork Builder Utility.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This module provides utilities for constructing advanced Google dork queries.
It includes enumerations for file types, dork operators, and content locations,
as well as a builder class to construct complex queries with various constraints.

Classes
-------
FileType : Enum
    Enumeration of supported file types.
DorkOperator : Enum
    Enumeration of dork operators.
ContentLocation : Enum
    Enumeration of content location specifiers.
GoogleDorkBuilder : class
    A builder class for constructing Google dork queries.

Functions
---------
create_complex_dork(phrases, sites_to_include, sites_to_exclude, file_types,
                    title_terms, url_terms, date_range)
    Create a complex Google dork query using multiple parameters.
"""

import datetime
import enum
import re
import typing


class FileType(
    str,
    enum.Enum,
):
    """
    Enumeration of supported file types for Google dork queries.

    Attributes
    ----------
    PDF : str
        PDF file type.
    SQL : str
        SQL file type.
    XLS : str
        XLS file type.
    XLSX : str
        XLSX file type.
    DOC : str
        DOC file type.
    DOCX : str
        DOCX file type.
    TXT : str
        TXT file type.
    LOG : str
        LOG file type.
    CONFIG : str
        CONFIG file type.
    BAK : str
        BAK file type.
    BACKUP : str
        BACKUP file type.
    CSV : str
        CSV file type.
    """

    PDF = "pdf"
    SQL = "sql"
    XLS = "xls"
    XLSX = "xlsx"
    DOC = "doc"
    DOCX = "docx"
    TXT = "txt"
    LOG = "log"
    CONFIG = "config"
    BAK = "bak"
    BACKUP = "backup"
    CSV = "csv"


class DorkOperator(
    str,
    enum.Enum,
):
    """
    Enumeration of dork operators used to combine search query terms.

    Attributes
    ----------
    AND : str
        Represents the AND operator (a space in Google queries).
    OR : str
        Represents the OR operator.
    NOT : str
        Represents the NOT operator.
    """

    AND = " "  # Space represents AND in Google
    OR = " OR "
    NOT = " -"


class ContentLocation(
    str,
    enum.Enum,
):
    """
    Enumeration of content location specifiers for Google dork queries.

    Attributes
    ----------
    TITLE : str
        Searches within the title.
    ALL_TITLE : str
        Searches within all title words.
    TEXT : str
        Searches within the text.
    ALL_TEXT : str
        Searches within all text.
    URL : str
        Searches within the URL.
    ALL_URL : str
        Searches within all URL components.
    ANCHOR : str
        Searches within anchor text.
    ALL_ANCHOR : str
        Searches within all anchor text.
    """

    TITLE = "intitle:"
    ALL_TITLE = "allintitle:"
    TEXT = "intext:"
    ALL_TEXT = "allintext:"
    URL = "inurl:"
    ALL_URL = "allinurl:"
    ANCHOR = "inanchor:"
    ALL_ANCHOR = "allinanchor:"


class GoogleDorkBuilder:
    """
    Builder class for constructing Google dork queries.

    Attributes
    ----------
    _query_parts : List[str]
        List of query components.
    _excluded_sites : List[str]
        List of domains to exclude.
    _included_sites : List[str]
        List of domains to include.
    _file_types : List[FileType]
        List of file types to constrain the search.
    _date_range : Optional[Dict[str, datetime.date]]
        Optional date range for search results.
    """

    def __init__(
        self,
    ) -> None:
        """Initialize a new instance of GoogleDorkBuilder."""
        self._query_parts: typing.List[str] = []
        self._excluded_sites: typing.List[str] = []
        self._included_sites: typing.List[str] = []
        self._file_types: typing.List[FileType] = []
        self._date_range: typing.Optional[
            typing.Dict[
                str,
                datetime.date,
            ]
        ] = None

    def add_exact_phrase(
        self,
        phrase: str,
    ) -> "GoogleDorkBuilder":
        """
        Add an exact phrase search to the query.

        Parameters
        ----------
        phrase : str
            The exact phrase to search for.

        Returns
        -------
        GoogleDorkBuilder
            The builder instance.

        Raises
        ------
        ValueError
            If the phrase is empty.
        """
        if not phrase:
            raise ValueError("Phrase cannot be empty")
        # Escape any existing quotes and add surrounding quotes
        cleaned_phrase = f'"{phrase.replace(r"\'", "")}"'
        self._query_parts.append(cleaned_phrase)
        return self

    def add_wildcard_phrase(
        self,
        prefix: str,
        suffix: str,
    ) -> "GoogleDorkBuilder":
        """
        Add a wildcard phrase search to the query.

        Parameters
        ----------
        prefix : str
            The prefix of the phrase.
        suffix : str
            The suffix of the phrase.

        Returns
        -------
        GoogleDorkBuilder
            The builder instance.

        Raises
        ------
        ValueError
            If both prefix and suffix are empty.
        """
        if not prefix and not suffix:
            raise ValueError(
                "At least one of prefix or suffix must be provided"
            )
        cleaned = f'"{prefix} * {suffix}"'.strip()
        self._query_parts.append(cleaned)
        return self

    def exclude_site(
        self,
        site: str,
    ) -> "GoogleDorkBuilder":
        """
        Exclude a specific site from search results.

        Parameters
        ----------
        site : str
            The domain to exclude.

        Returns
        -------
        GoogleDorkBuilder
            The builder instance.

        Raises
        ------
        ValueError
            If the domain is invalid.
        """
        if not self._is_valid_domain(site):
            raise ValueError(f"Invalid domain: {site}")
        self._excluded_sites.append(site)
        return self

    def include_site(
        self,
        site: str,
    ) -> "GoogleDorkBuilder":
        """
        Include only specific sites in search results.

        Parameters
        ----------
        site : str
            The domain to include.

        Returns
        -------
        GoogleDorkBuilder
            The builder instance.

        Raises
        ------
        ValueError
            If the domain is invalid.
        """
        if not self._is_valid_domain(site):
            raise ValueError(f"Invalid domain: {site}")
        self._included_sites.append(site)
        return self

    def add_file_type(
        self,
        file_type: FileType,
    ) -> "GoogleDorkBuilder":
        """
        Add a file type constraint to the search query.

        Parameters
        ----------
        file_type : FileType
            The file type to add.

        Returns
        -------
        GoogleDorkBuilder
            The builder instance.
        """
        self._file_types.append(file_type)
        return self

    def add_content_location(
        self,
        content: str,
        location: ContentLocation,
        operator: DorkOperator = DorkOperator.AND,
    ) -> "GoogleDorkBuilder":
        """
        Add content with a specific location constraint to the query.

        Parameters
        ----------
        content : str
            The content to search for.
        location : ContentLocation
            The location where the content should be searched.
        operator : DorkOperator, optional
            The operator to prepend (default is DorkOperator.AND).

        Returns
        -------
        GoogleDorkBuilder
            The builder instance.

        Raises
        ------
        ValueError
            If the content is empty.
        """
        if not content:
            raise ValueError("Content cannot be empty")
        self._query_parts.append(f"{operator.value}{location.value}{content}")
        return self

    def set_date_range(
        self,
        start_date: typing.Union[
            datetime.date,
            datetime.datetime,
            str,
        ],
        end_date: typing.Union[
            datetime.date,
            datetime.datetime,
            str,
        ],
    ) -> "GoogleDorkBuilder":
        """
        Set a date range for search results.

        Parameters
        ----------
        start_date : datetime.date, datetime.datetime, or str
            The start date in YYYY-MM-DD format or as a date/datetime object.
        end_date : datetime.date, datetime.datetime, or str
            The end date in YYYY-MM-DD format or as a date/datetime object.

        Returns
        -------
        GoogleDorkBuilder
            The builder instance.

        Raises
        ------
        ValueError
            If the start date is after the end date.
        """
        start = self._parse_date(start_date)
        end = self._parse_date(end_date)
        if start > end:
            raise ValueError("Start date must be before end date")
        self._date_range = {
            "start": start,
            "end": end,
        }
        return self

    def add_around(
        self,
        term1: str,
        term2: str,
        words: int = 5,
    ) -> "GoogleDorkBuilder":
        """
        Add an AROUND operator to find two terms near each other.

        Parameters
        ----------
        term1 : str
            The first term.
        term2 : str
            The second term.
        words : int, optional
            The maximum number of words between the terms (default is 5).

        Returns
        -------
        GoogleDorkBuilder
            The builder instance.

        Raises
        ------
        ValueError
            If the number of words is less than 1.
        """
        if words < 1:
            raise ValueError("Words must be positive")
        self._query_parts.append(f"{term1} AROUND({words}) {term2}")
        return self

    def build(
        self,
    ) -> str:
        """
        Build the final Google dork query.

        Returns
        -------
        str
            The complete query string.
        """
        query_parts = self._query_parts.copy()

        # Add site constraints for inclusion
        if self._included_sites:
            sites = " OR ".join(
                f"site:{site}" for site in self._included_sites
            )
            query_parts.append(f"({sites})")

        # Add site constraints for exclusion
        for site in self._excluded_sites:
            query_parts.append(f"-site:{site}")

        # Add file type constraints
        if self._file_types:
            file_types = " OR ".join(
                f"filetype:{ft.value}" for ft in self._file_types
            )
            query_parts.append(f"({file_types})")

        # Add date range constraints if specified
        if self._date_range:
            start_str = self._date_range["start"].strftime("%Y-%m-%d")
            end_str = self._date_range["end"].strftime("%Y-%m-%d")
            query_parts.append(f"after:{start_str} before:{end_str}")

        # Combine all parts with spaces as the AND operator
        return " ".join(part for part in query_parts if part)

    @staticmethod
    def _is_valid_domain(
        domain: str,
    ) -> bool:
        """
        Validate the format of a domain.

        Parameters
        ----------
        domain : str
            The domain to validate.

        Returns
        -------
        bool
            True if the domain is valid, False otherwise.
        """
        pattern = (
            r"^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$"
        )
        return bool(
            re.match(
                pattern,
                domain,
            )
        )

    @staticmethod
    def _parse_date(
        d: typing.Union[
            datetime.date,
            datetime.datetime,
            str,
        ],
    ) -> datetime.date:
        """
        Parse a date input into a datetime.date object.

        Parameters
        ----------
        d : datetime.date, datetime.datetime, or str
            The date input. If a string, the expected format is YYYY-MM-DD.

        Returns
        -------
        datetime.date
            The parsed date object.

        Raises
        ------
        ValueError
            If the string is not in the expected format.
        TypeError
            If the input type is unsupported.
        """
        if isinstance(
            d,
            datetime.datetime,
        ):
            return d.date()
        if isinstance(
            d,
            datetime.date,
        ):
            return d
        if isinstance(
            d,
            str,
        ):
            try:
                return datetime.datetime.strptime(
                    d,
                    "%Y-%m-%d",
                ).date()
            except ValueError as e:
                raise ValueError(
                    f"Invalid date format. Use YYYY-MM-DD: {d}"
                ) from e
        raise TypeError(f"Unsupported date type: {type(d)}")


def create_complex_dork(
    phrases: typing.List[str],
    sites_to_include: typing.List[str],
    sites_to_exclude: typing.List[str],
    file_types: typing.List[FileType],
    title_terms: typing.List[str],
    url_terms: typing.List[str],
    date_range: typing.Optional[
        typing.Dict[
            str,
            str,
        ]
    ] = None,
) -> str:
    """
    Create a complex Google dork query with multiple parameters.

    Parameters
    ----------
    phrases : List[str]
        List of exact phrases to search.
    sites_to_include : List[str]
        List of domains to include.
    sites_to_exclude : List[str]
        List of domains to exclude.
    file_types : List[FileType]
        List of file types to include.
    title_terms : List[str]
        List of terms to search for in titles.
    url_terms : List[str]
        List of terms to search for in URLs.
    date_range : Dict[str, str], optional
        A dictionary with 'start' and 'end' dates in YYYY-MM-DD format.

    Returns
    -------
    str
        The constructed Google dork query string.

    Raises
    ------
    ValueError
        If there is an error creating the dork query.
    """
    try:
        builder = GoogleDorkBuilder()

        # Add exact phrases
        for phrase in phrases:
            builder.add_exact_phrase(phrase)

        # Include specific sites
        for site in sites_to_include:
            builder.include_site(site)

        # Exclude specific sites
        for site in sites_to_exclude:
            builder.exclude_site(site)

        # Add file types
        for file_type in file_types:
            builder.add_file_type(file_type)

        # Add title search terms
        for term in title_terms:
            builder.add_content_location(
                term,
                ContentLocation.TITLE,
            )

        # Add URL search terms
        for term in url_terms:
            builder.add_content_location(
                term,
                ContentLocation.URL,
            )

        # Set date range if provided
        if date_range:
            builder.set_date_range(
                date_range.get(
                    "start",
                    "2020-01-01",
                ),
                date_range.get(
                    "end",
                    datetime.datetime.now().strftime("%Y-%m-%d"),
                ),
            )

        return builder.build()
    except Exception as e:
        raise ValueError(f"Error creating dork: {e}") from e
