import base64
import contextlib
import datetime
import io
import platform
import queue
import re
import socket
import ssl
import subprocess
import threading
import typing
import urllib.parse
import xml.etree.ElementTree as ElementTree

import dns.query
import dns.resolver
import dns.zone
import folium
import langdetect
import matplotlib.pyplot
import networkx
import nltk
import nltk.sentiment.vader
import numpy
import PIL.Image
import pytesseract
import sentence_transformers
import sklearn.decomposition
import sklearn.feature_extraction.text
import spacy
import spacy.tokens
import speech_recognition
import tensorflow as tf
import tensorflow_hub
import transformers
import wordcloud

import chitragupt.modules.browsers as browser


def whois_lookup(
    domain: str,
) -> str:
    """
    Performs a WHOIS lookup for the specified domain using a two-step query:
    first querying IANA and then a referral WHOIS server if available.

    :param domain: The domain name to query.
    :return: The WHOIS result as a text string.
    """
    # First query the IANA WHOIS server
    result: str = ""
    try:
        sock: socket.socket = socket.socket(
            socket.AF_INET,
            socket.SOCK_STREAM,
        )
        sock.settimeout(10)
        sock.connect(
            (
                "whois.iana.org",
                43,
            )
        )
        sock.sendall((domain + "\r\n").encode("utf-8"))
        response: bytes = b""
        while True:
            data: bytes = sock.recv(4096)
            if not data:
                break
            response += data
        sock.close()
        result = response.decode("utf-8")
    except Exception as e:
        result = "Error querying IANA WHOIS: " + str(e)

    # Look for a referral server in the IANA response.
    referral: typing.Optional[typing.Match] = re.search(
        r"refer:\s*(\S+)",
        result,
        re.IGNORECASE,
    )
    if referral:
        ref_server: str = referral.group(1)
        try:
            sock2: socket.socket = socket.socket(
                socket.AF_INET,
                socket.SOCK_STREAM,
            )
            sock2.settimeout(10)
            sock2.connect(
                (
                    ref_server,
                    43,
                )
            )
            sock2.sendall((domain + "\r\n").encode("utf-8"))
            response2: bytes = b""
            while True:
                data2: bytes = sock2.recv(4096)
                if not data2:
                    break
                response2 += data2
            sock2.close()
            result = response2.decode("utf-8")
        except Exception as e:
            result += (
                "\nError querying referral WHOIS ("
                + ref_server
                + "): "
                + str(e)
            )
    return result


def dns_lookup(
    domain: str,
) -> typing.Any:
    """
    Performs a basic DNS lookup for the given domain using the standard socket library.

    :param domain: The domain name to resolve.
    :return: A dictionary containing the canonical hostname, alias list, and IP addresses.
    """
    try:
        (
            hostname,
            aliaslist,
            ipaddrlist,
        ) = socket.gethostbyname_ex(domain)
        return {
            "hostname": hostname,
            "aliases": aliaslist,
            "ip_addresses": ipaddrlist,
        }
    except Exception as e:
        return {"error": str(e)}


def get_ssl_certificate_info(
    domain: str,
    port: int = 443,
) -> typing.Any:
    try:
        context: ssl.SSLContext = ssl.create_default_context()
        with (
            socket.create_connection(
                (
                    domain,
                    port,
                ),
                timeout=10,
            ) as sock,
            context.wrap_socket(
                sock,
                server_hostname=domain,
            ) as ssock,
        ):
            cert: typing.Any = ssock.getpeercert()

        if "notBefore" in cert and "notAfter" in cert:
            with contextlib.suppress(Exception):
                cert["notBefore_parsed"] = datetime.datetime.strptime(
                    cert["notBefore"],
                    "%b %d %H:%M:%S %Y %Z",
                )
                cert["notAfter_parsed"] = datetime.datetime.strptime(
                    cert["notAfter"],
                    "%b %d %H:%M:%S %Y %Z",
                )
        return cert
    except Exception as e:
        return {"error": str(e)}


def extract_ip_addresses_from_text(
    text: str,
) -> typing.List[str]:
    """
    Extracts IPv4 addresses from the provided text.

    :param text: The text to search.
    :return: A list of unique IPv4 addresses.
    """
    ips: typing.List[str] = re.findall(
        r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b",
        text,
    )
    return list(set(ips))


def check_port_status(
    ip: str,
    port: int,
    timeout: float = 3.0,
) -> bool:
    """
    Checks whether a specific TCP port is open on the target IP address.

    :param ip: The IP address to check.
    :param port: The port number.
    :param timeout: Connection timeout in seconds (default 3.0).
    :return: True if the port is open; False otherwise.
    """
    try:
        sock: socket.socket = socket.socket(
            socket.AF_INET,
            socket.SOCK_STREAM,
        )
        sock.settimeout(timeout)
        result: int = sock.connect_ex(
            (
                ip,
                port,
            )
        )
        sock.close()
        return result == 0
    except Exception:
        return False


def perform_topic_modeling(
    documents: typing.List[str],
    num_topics: int = 5,
    num_top_words: int = 5,
) -> typing.Any:
    """
    Performs topic modeling on a list of documents using TF-IDF vectorization and NMF.
    Returns a list of topics with their top words.
    """
    try:
        vectorizer: sklearn.feature_extraction.text.TfidfVectorizer = (
            sklearn.feature_extraction.text.TfidfVectorizer(
                stop_words="english"
            )
        )
        tfidf: typing.Any = vectorizer.fit_transform(documents)
        nmf: sklearn.decomposition.NMF = sklearn.decomposition.NMF(
            n_components=num_topics,
            random_state=1,
        )
        nmf.fit(tfidf)
        feature_names: typing.List[str] = (
            vectorizer.get_feature_names_out().tolist()
        )
        topics: typing.List[
            typing.Dict[
                str,
                typing.Any,
            ]
        ] = []
        for (
            topic_idx,
            topic,
        ) in enumerate(nmf.components_):
            top_features: typing.List[str] = [
                feature_names[i]
                for i in topic.argsort()[: -num_top_words - 1 : -1]
            ]
            topics.append(
                {
                    "topic": topic_idx,
                    "top_words": top_features,
                }
            )
        return topics
    except Exception as e:
        return {"error": str(e)}


def generate_text_summary(
    text: str,
    num_sentences: int = 3,
) -> str:
    """
    Generates a naive summary of the provided text by returning the first few sentences.
    This is a simple implementation based on sentence tokenization.
    """
    try:
        sentences: typing.List[str] = nltk.tokenize.sent_tokenize(text)
        summary: str = " ".join(sentences[:num_sentences])
        return summary
    except Exception as e:
        return "Error: " + str(e)


def get_map_from_geolocations(
    locations: typing.List[
        typing.Dict[
            str,
            typing.Any,
        ]
    ],
) -> typing.Any:
    """
    Generates an interactive map from a list of geolocations using Folium.
    Each location should be a dictionary containing 'lat', 'lon', and optionally 'label'.
    The rendered HTML is encoded in base64.
    """
    try:
        if not locations:
            return {"error": "No locations provided"}
        center_lat: float = float(
            locations[0].get(
                "lat",
                0,
            )
        )
        center_lon: float = float(
            locations[0].get(
                "lon",
                0,
            )
        )
        fmap: folium.Map = folium.Map(
            location=[
                center_lat,
                center_lon,
            ],
            zoom_start=5,
        )
        for loc in locations:
            lat: float = float(
                loc.get(
                    "lat",
                    0,
                )
            )
            lon: float = float(
                loc.get(
                    "lon",
                    0,
                )
            )
            label: str = str(
                loc.get(
                    "label",
                    "",
                )
            )
            folium.Marker(
                location=[
                    lat,
                    lon,
                ],
                popup=label,
            ).add_to(fmap)
        html_data: str = fmap.get_root().render()
        encoded_html: str = base64.b64encode(html_data.encode("utf-8")).decode(
            "utf-8"
        )
        return {"map_html_base64": encoded_html}
    except Exception as e:
        return {"error": str(e)}


def analyze_sentiment_nltk(
    text: str,
) -> typing.Any:
    """
    Analyzes the sentiment of the provided text using NLTK's VADER sentiment analyzer.
    Returns a dictionary of sentiment scores.
    """
    try:
        analyzer: nltk.sentiment.vader.SentimentIntensityAnalyzer = (
            nltk.sentiment.vader.SentimentIntensityAnalyzer()
        )
        scores: typing.Any = analyzer.polarity_scores(text)
        return scores
    except Exception as e:
        return {"error": str(e)}


def get_cname_records(
    domain: str,
) -> typing.Any:
    """Retrieves the CNAME record for the specified domain."""
    try:
        answers = dns.resolver.resolve(
            domain,
            "CNAME",
        )
        return [str(rdata.target).rstrip(".") for rdata in answers]  # type: ignore
    except Exception as e:
        return {"error": str(e)}


def detect_language(
    text: str,
) -> typing.Any:
    """
    Detects the language of the provided text using the langdetect library.

    :param text: The input text.
    :return: The detected language code (e.g., 'en') or an error.
    """
    try:
        language: str = langdetect.detect(text)
        return language
    except Exception as e:
        return {"error": str(e)}


def generate_keyword_cloud(
    text: str,
) -> typing.Any:
    """
    Generates a keyword cloud image from the provided text using the WordCloud library.
    The resulting image is encoded in base64.

    :param text: The input text.
    :return: A dictionary containing the base64-encoded PNG image, or an error.
    """
    try:
        stopwords: typing.Set[str] = set(
            nltk.corpus.stopwords.words("english")
        )
        wc: wordcloud.WordCloud = wordcloud.WordCloud(
            width=800,
            height=400,
            background_color="white",
            stopwords=stopwords,
        )
        wc.generate(text)
        image: PIL.Image.Image = wc.to_image()
        buffer: io.BytesIO = io.BytesIO()
        image.save(
            buffer,
            format="PNG",
        )
        encoded_image: str = base64.b64encode(buffer.getvalue()).decode(
            "utf-8"
        )
        return {"image_base64": encoded_image}
    except Exception as e:
        return {"error": str(e)}


def check_email_format(
    email: str,
) -> bool:
    """
    Validates whether the provided string is in a proper email format.

    :param email: The email address to validate.
    :return: True if valid; False otherwise.
    """
    pattern: str = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
    return (
        re.match(
            pattern,
            email,
        )
        is not None
    )


def extract_mentions(
    text: str,
) -> typing.List[str]:
    """
    Extracts Twitter-style mentions (usernames prefixed with @) from the provided text.

    :param text: The text to search.
    :return: A list of unique mentions.
    """
    mentions: typing.List[str] = re.findall(
        r"@\w+",
        text,
    )
    return list(set(mentions))


def extract_bitcoin_addresses(
    text: str,
) -> typing.List[str]:
    """
    Extracts Bitcoin addresses from the provided text.
    Bitcoin addresses typically start with '1', '3', or 'bc1'.
    """
    pattern: str = (
        r"\b(?:[13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-z0-9]{39,59})\b"
    )
    addresses: typing.List[str] = re.findall(
        pattern,
        text,
    )
    return list(set(addresses))


def extract_ethereum_addresses(
    text: str,
) -> typing.List[str]:
    """
    Extracts Ethereum addresses from the provided text.
    Ethereum addresses start with '0x' followed by 40 hexadecimal characters.
    """
    pattern: str = r"\b0x[a-fA-F0-9]{40}\b"
    addresses: typing.List[str] = re.findall(
        pattern,
        text,
    )
    return list(set(addresses))


def get_txt_records(
    domain: str,
) -> typing.Any:
    """Retrieves the TXT records for the specified domain."""
    try:
        answers = dns.resolver.resolve(
            domain,
            "TXT",
        )
        records: typing.List[str] = []
        for rdata in answers:
            # Concatenate TXT record parts if necessary.
            txt_record = "".join(
                [
                    part.decode("utf-8")
                    if isinstance(
                        part,
                        bytes,
                    )
                    else str(part)
                    for part in rdata.strings  # type: ignore
                ]
            )
            records.append(txt_record)
        return records
    except Exception as e:
        return {"error": str(e)}


def get_ns_records(
    domain: str,
) -> typing.Any:
    """Retrieves the NS (Name Server) records for the specified domain."""
    try:
        answers = dns.resolver.resolve(
            domain,
            "NS",
        )
        return [str(rdata.target).rstrip(".") for rdata in answers]  # type: ignore
    except Exception as e:
        return {"error": str(e)}


def get_domain_age(
    domain: str,
) -> typing.Any:
    """Estimates the age of the domain by parsing its WHOIS data for the creation date."""
    try:
        whois_data: str = whois_lookup(domain)
        # Try multiple patterns for the creation date.
        match = re.search(
            r"Creation Date:\s*(.+)",
            whois_data,
            re.IGNORECASE,
        ) or re.search(
            r"Created On:\s*(.+)",
            whois_data,
            re.IGNORECASE,
        )
        if not match:
            return {"error": "Creation date not found in WHOIS data"}
        creation_date_str: str = match.group(1).strip()
        creation_date: typing.Optional[datetime.datetime] = None
        try:
            creation_date = datetime.datetime.fromisoformat(creation_date_str)
        except Exception:
            try:
                creation_date = datetime.datetime.strptime(
                    creation_date_str,
                    "%Y-%m-%d",
                )
            except Exception:
                with contextlib.suppress(Exception):
                    creation_date = datetime.datetime.strptime(
                        creation_date_str,
                        "%d-%b-%Y",
                    )
        if not creation_date:
            return {
                "error": "Unable to parse creation date: " + creation_date_str
            }
        age = datetime.datetime.now() - creation_date
        return {
            "creation_date": creation_date.isoformat(),
            "age_days": age.days,
        }
    except Exception as e:
        return {"error": str(e)}


def get_mx_records(
    domain: str,
) -> typing.Any:
    try:
        answers = dns.resolver.resolve(
            domain,
            "MX",
        )
        records: typing.List[
            typing.Dict[
                str,
                typing.Union[
                    str,
                    int,
                    None,
                ],
            ]
        ] = [
            {
                "exchange": str(rdata.exchange).rstrip("."),  # type: ignore
                "preference": getattr(
                    rdata,
                    "preference",
                    getattr(
                        rdata,
                        "priority",
                        None,
                    ),
                ),
            }
            for rdata in answers
        ]
        return records
    except Exception as e:
        return {"error": str(e)}


def detect_web_technologies(
    html: str,
) -> typing.List[str]:
    """
    Detects web technologies used on a site by scanning HTML content for common keywords.
    Returns a sorted list of detected technologies.
    """
    tech_signatures: typing.Dict[
        str,
        str,
    ] = {
        "wp-content": "WordPress",
        "Drupal.settings": "Drupal",
        "Joomla": "Joomla",
        "Shopify": "Shopify",
        "Magento": "Magento",
        "Wix": "Wix",
        "Squarespace": "Squarespace",
        "React": "React",
        "Angular": "Angular",
        "Vue.js": "Vue.js",
        "Bootstrap": "Bootstrap",
        "Laravel": "Laravel",
        "ASP.NET": "ASP.NET",
    }
    lower_html: str = html.lower()
    detected: typing.Set[str] = {
        tech
        for keyword, tech in tech_signatures.items()
        if keyword.lower() in lower_html
    }
    return sorted(detected)


def extract_keywords(
    text: str,
    num_keywords: int = 5,
) -> typing.List[typing.Any]:
    """
    Extracts keywords from the provided text using a simple frequency analysis with NLTK.
    Returns a list of the top 'num_keywords' words (excluding stopwords).
    """
    try:
        tokens: typing.List[str] = nltk.tokenize.word_tokenize(text)
        words: typing.List[str] = [
            word.lower() for word in tokens if word.isalpha()
        ]
        stopwords: typing.Set[str] = set(
            nltk.corpus.stopwords.words("english")
        )
        filtered_words: typing.List[str] = [
            word for word in words if word not in stopwords
        ]
        freq_dist: nltk.probability.FreqDist = nltk.FreqDist(filtered_words)
        keywords: typing.List[str] = [
            word for word, count in freq_dist.most_common(num_keywords)
        ]
        return keywords
    except Exception as e:
        return [{"error": str(e)}]


def perform_named_entity_recognition(
    text: str,
) -> typing.Any:
    """
    Uses NLTK to perform named entity recognition (NER) on the provided text.
    Returns a list of named entities along with their entity type.
    """
    try:
        tokens: typing.List[str] = nltk.tokenize.word_tokenize(text)
        pos_tags: typing.List[
            typing.Tuple[
                str,
                str,
            ]
        ] = nltk.tag.pos_tag(tokens)
        chunks: typing.Any = nltk.chunk.ne_chunk(pos_tags)
        entities: typing.List[
            typing.Tuple[
                str,
                str,
            ]
        ] = []
        for chunk in chunks:
            if hasattr(
                chunk,
                "label",
            ):
                entity: str = " ".join(item[0] for item in chunk)
                entities.append(
                    (
                        chunk.label(),
                        entity,
                    )
                )
        return entities
    except Exception as e:
        return {"error": str(e)}


def extract_emails_from_text(
    text: str,
) -> typing.List[str]:
    """
    Extracts email addresses from the provided text.

    :param text: The text to search.
    :return: A list of email addresses.
    """
    emails: typing.List[str] = re.findall(
        r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
        text,
    )
    return list(set(emails))


def extract_urls_from_text(
    text: str,
) -> typing.List[str]:
    """
    Extracts URLs from the provided text.

    :param text: The text to search.
    :return: A list of URLs.
    """
    # This regex matches http(s) URLs.
    urls: typing.List[str] = re.findall(
        r"https?://[^\s'\"<>]+",
        text,
    )
    return list(set(urls))


def check_subdomain_takeover(
    subdomain: str,
) -> typing.Any:
    """
    Checks if the specified subdomain might be vulnerable to subdomain takeover.
    This is done by inspecting its CNAME record for known vulnerable targets.
    """
    try:
        answers = dns.resolver.resolve(
            subdomain,
            "CNAME",
        )
        for rdata in answers:
            cname: str = str(rdata.target).rstrip(".")  # type: ignore
            vulnerable_signatures = [
                "herokudns.com",
                "aws.amazon.com",
                "github.io",
                "wordpress.com",
                "cloudfront.net",
                "azurewebsites.net",
            ]
            for sig in vulnerable_signatures:
                if sig in cname:
                    return {
                        "subdomain": subdomain,
                        "cname": cname,
                        "vulnerable": True,
                    }
        return {
            "subdomain": subdomain,
            "vulnerable": False,
        }
    except dns.resolver.NoAnswer:
        return {
            "subdomain": subdomain,
            "error": "No CNAME record found",
        }
    except Exception as e:
        return {
            "subdomain": subdomain,
            "error": str(e),
        }


def extract_phone_numbers_from_text(
    text: str,
) -> typing.List[str]:
    """
    Extracts phone numbers from the provided text.
    A simple regex is used to match numbers in various common formats.
    """
    phones: typing.List[str] = re.findall(
        r"\+?\d[\d\s\-\(\)]{7,}\d",
        text,
    )
    return list(set(phones))


def attempt_zone_transfer(
    domain: str,
) -> typing.Any:
    """
    Attempts a DNS zone transfer for the given domain using its NS records.
    Returns zone records if successful.
    """
    ns_records: typing.Any = get_ns_records(domain)
    if (
        isinstance(
            ns_records,
            dict,
        )
        and "error" in ns_records
    ):
        return ns_records
    for ns in ns_records:
        try:
            xfr: typing.Any = dns.query.xfr(
                ns,
                domain,
                timeout=10,
            )
            zone: dns.zone.Zone = dns.zone.from_xfr(xfr)
            records: typing.Dict[
                str,
                typing.List[str],
            ] = {}
            for (
                name,
                node,
            ) in zone.nodes.items():
                record_texts: typing.List[str] = []
                for rdataset in node.rdatasets:
                    for r in rdataset:
                        record_texts.append(r.to_text())
                records[str(name)] = record_texts
            return {
                "nameserver": ns,
                "zone_records": records,
            }
        except Exception:
            continue
    return {"error": "Zone transfer failed for all nameservers."}


def validate_email_with_mx(
    email: str,
) -> bool:
    """
    Validates an email address by checking its format and ensuring its domain has MX records.
    Returns True if valid and the domain resolves MX records.
    """
    if not check_email_format(email):
        return False
    try:
        domain: str = email.split("@")[1]
        answers: typing.Any = dns.resolver.resolve(
            domain,
            "MX",
        )
        return bool(answers)
    except Exception:
        return False


def extract_hashtags(
    text: str,
) -> typing.List[str]:
    """
    Extracts hashtags from the provided text.

    :param text: The text to search.
    :return: A list of unique hashtags.
    """
    hashtags: typing.List[str] = re.findall(
        r"#\w+",
        text,
    )
    return list(set(hashtags))


def check_dnssec(
    domain: str,
) -> bool:
    """
    Checks if the domain is DNSSEC enabled by attempting to retrieve DNSKEY records.
    Returns True if DNSKEY records are found, otherwise False.
    """
    try:
        answers: typing.Any = dns.resolver.resolve(
            domain,
            "DNSKEY",
        )
        return bool(answers)
    except Exception:
        return False


def concurrent_scan_ports(
    ip: str,
    ports: typing.List[int],
) -> typing.Dict[
    int,
    bool,
]:
    """
    Performs a concurrent port scan on the specified IP address.
    Returns a dictionary mapping each port to its open (True) or closed (False) status.
    """
    results: typing.Dict[
        int,
        bool,
    ] = {}
    port_queue: queue.Queue = queue.Queue()
    for port in ports:
        port_queue.put(port)

    def worker() -> None:
        while True:
            try:
                port: int = port_queue.get_nowait()
            except Exception:
                break
            results[port] = check_port_status(
                ip,
                port,
            )
            port_queue.task_done()

    threads: typing.List[threading.Thread] = []
    for i in range(10):
        t: threading.Thread = threading.Thread(target=worker)
        threads.append(t)
        t.start()

    port_queue.join()
    for t in threads:
        t.join()
    return results


def fetch_social_media_profiles(
    text: str,
) -> typing.List[str]:
    """
    Extracts social media profile URLs from the provided text.
    Looks for common platforms such as Twitter, Facebook, Instagram, LinkedIn, and YouTube.
    """
    pattern: str = r"https?://(?:www\.)?(?:twitter\.com|facebook\.com|instagram\.com|linkedin\.com|youtube\.com)/[^\s'\"<>]+"
    profiles: typing.List[str] = re.findall(
        pattern,
        text,
        re.IGNORECASE,
    )
    return list(set(profiles))


def get_google_cache_url(
    url: str,
) -> str:
    """Constructs a Google Cache URL for the specified page."""
    return "https://webcache.googleusercontent.com/search?q=cache:" + url


def scan_ports(
    ip: str,
    ports: typing.List[int],
) -> typing.Any:
    """
    Scans a list of TCP ports on the specified IP address.
    Returns a dictionary mapping each port to a boolean (open or closed).
    """
    results: typing.Dict[
        int,
        bool,
    ] = {
        port: check_port_status(
            ip,
            port,
        )
        for port in ports
    }
    return results


def get_banner(
    ip: str,
    port: int,
) -> str:
    """
    Attempts to retrieve a banner from the specified IP and port.
    Useful for service fingerprinting.
    """
    try:
        sock: socket.socket = socket.socket(
            socket.AF_INET,
            socket.SOCK_STREAM,
        )
        sock.settimeout(5)
        sock.connect(
            (
                ip,
                port,
            )
        )
        banner: bytes = sock.recv(1024)
        sock.close()
        return banner.decode(
            "utf-8",
            errors="ignore",
        )
    except Exception as e:
        return "Error: " + str(e)


def get_ssl_expiration_date(
    domain: str,
    port: int = 443,
) -> typing.Any:
    """
    Retrieves the SSL certificate expiration date and returns the number of days until expiration.

    :param domain: The domain name.
    :param port: The port number (default 443).
    :return: A dictionary containing the expiration date and days remaining, or an error.
    """
    cert_info: typing.Any = get_ssl_certificate_info(
        domain,
        port,
    )
    if "error" in cert_info:
        return cert_info
    if "notAfter" not in cert_info:
        return {"error": "Certificate expiry information not available"}
    try:
        expiry_str: str = cert_info["notAfter"]
        expiry_date: typing.Optional[datetime.datetime] = None
        # Try multiple common date formats.
        for fmt in (
            "%b %d %H:%M:%S %Y %Z",
            "%Y-%m-%dT%H:%M:%SZ",
        ):
            try:
                expiry_date = datetime.datetime.strptime(
                    expiry_str,
                    fmt,
                )
                break
            except Exception:
                continue
        if expiry_date is None:
            return {
                "error": "Could not parse expiry date format: " + expiry_str
            }
        days_left: int = (
            expiry_date - datetime.datetime.now(datetime.timezone.utc)
        ).days
        return {
            "expiration_date": expiry_date.isoformat(),
            "days_until_expiration": days_left,
        }
    except Exception as e:
        return {"error": str(e)}


def extract_meta_tags(
    html: str,
) -> typing.Dict[
    str,
    str,
]:
    """
    Extracts meta tag information from HTML content.

    :param html: The HTML content as a string.
    :return: A dictionary with meta tag names or properties as keys and their content as values.
    """
    meta_tags: typing.Dict[
        str,
        str,
    ] = {}
    matches: typing.List[typing.Any] = re.findall(
        r'<meta\s+[^>]*(?:name|property)="([^"]+)"\s+content="([^"]+)"',
        html,
        re.IGNORECASE,
    )
    for match in matches:
        meta_tags[match[0]] = match[1]
    return meta_tags


def trace_route(
    target: str,
) -> typing.Any:
    """
    Performs a traceroute to the specified target using the system's traceroute (or tracert) command.

    :param target: The target domain or IP address.
    :return: The traceroute output as a string, or an error.
    """
    try:
        os_name: str = platform.system().lower()
        if "windows" in os_name:
            command: typing.List[str] = [
                "tracert",
                "-d",
                target,
            ]
        else:
            command: typing.List[str] = [
                "traceroute",
                "-n",
                target,
            ]
        result: subprocess.CompletedProcess = subprocess.run(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            timeout=30,
        )
        return (
            result.stdout
            if result.returncode == 0
            else {"error": result.stderr}
        )
    except Exception as e:
        return {"error": str(e)}


class OSINTUtilityTool:
    """
    An OSINT (Open Source Intelligence) utility tool class featuring many functions
    for information gathering. No API keys are required and all operations use public,
    open endpoints. This implementation is extremely static and uses only absolute
    imports.
    """

    def __init__(
        self,
    ) -> None:
        """Initializes the OSINT utility tool with a requests session and a custom User-Agent."""
        self.use_model = None
        self.spacy_nlp = None
        self.semantic_model = None
        self.hf_translator = None
        self.hf_qa = None
        self.hf_sentiment = None
        self.hf_summarizer = None
        self.hf_summarizer = None
        self.tf_classification_model = None
        self.session: browser.BrowserBase = browser.BrowserBase()
        self.initialized: bool = True

    @staticmethod
    def reverse_dns_lookup(
        ip: str,
    ) -> typing.Any:
        """
        Performs a reverse DNS lookup for the specified IP address.

        :param ip: The IP address to reverse-resolve.
        :return: A dictionary with the primary hostname, alias list, and IP address list.
        """
        try:
            (
                hostname,
                aliaslist,
                ipaddrlist,
            ) = socket.gethostbyaddr(ip)
            return {
                "hostname": hostname,
                "aliases": aliaslist,
                "ip_addresses": ipaddrlist,
            }
        except Exception as e:
            return {"error": str(e)}

    def ip_geolocation(
        self,
        ip: str,
    ) -> typing.Any:
        """
        Retrieves geolocation information for the specified IP address using a free public API.

        :param ip: The IP address to geolocate.
        :return: A dictionary of geolocation data.
        """
        try:
            url: str = "http://ip-api.com/json/" + ip
            response = self.session.get(
                url=url,
                timeout=10,
            )
            if response.status_code == 200:
                return response.json()
            else:
                return {"error": "HTTP " + str(response.status_code)}
        except Exception as e:
            return {"error": str(e)}

    def get_public_ip(
        self,
    ) -> str:
        """
        Retrieves the public IP address of the machine running this tool.

        :return: The public IP address as a string.
        """
        try:
            url: str = "https://api.ipify.org?format=json"
            response = self.session.get(
                url=url,
                timeout=10,
            )
            data: typing.Any = response.json()
            return data.get(
                "ip",
                "",
            )
        except Exception as e:
            return "Error: " + str(e)

    def get_http_headers(
        self,
        url: str,
    ) -> typing.Any:
        """
        Retrieves HTTP headers from the specified URL.

        :param url: The target URL.
        :return: A dictionary of HTTP headers.
        """
        try:
            response = self.session.head(
                url,
                timeout=10,
            )
            return dict(response.headers)
        except Exception as e:
            return {"error": str(e)}

    def get_robots_txt(
        self,
        url: str,
    ) -> str:
        """
        Retrieves the robots.txt file from the target website.

        :param url: The domain or full URL of the website.
        :return: The contents of the robots.txt file as a string.
        """
        if not (url.startswith("http://") or url.startswith("https://")):
            url = "http://" + url
        if not url.rstrip("/").endswith("robots.txt"):
            url = url.rstrip("/") + "/robots.txt"
        try:
            response = self.session.get(
                url,
                timeout=10,
            )
            return response.text
        except Exception as e:
            return "Error: " + str(e)

    def get_sitemap(
        self,
        url: str,
    ) -> str:
        """
        Retrieves the sitemap.xml file from the target website.

        :param url: The domain or full URL of the website.
        :return: The contents of the sitemap.xml file as a string.
        """
        if not (url.startswith("http://") or url.startswith("https://")):
            url = "http://" + url
        if not url.rstrip("/").endswith("sitemap.xml"):
            url = url.rstrip("/") + "/sitemap.xml"
        try:
            response = self.session.get(
                url,
                timeout=10,
            )
            return response.text
        except Exception as e:
            return "Error: " + str(e)

    def fetch_page_title(
        self,
        url: str,
    ) -> str:
        """
        Fetches the HTML title of the web page at the specified URL.

        :param url: The target URL.
        :return: The page title as a string.
        """
        try:
            response = self.session.get(
                url,
                timeout=10,
            )
            html_text: str = response.text
            match: typing.Optional[typing.Match] = re.search(
                r"<title>(.*?)</title>",
                html_text,
                re.IGNORECASE | re.DOTALL,
            )
            return match.group(1).strip() if match else ""
        except Exception as e:
            return "Error: " + str(e)

    def get_asn_info(
        self,
        ip: str,
    ) -> typing.Any:
        """
        Retrieves ASN (Autonomous System Number) information for the specified IP address
        using a free public API.

        :param ip: The IP address.
        :return: A dictionary of ASN information.
        """
        try:
            url: str = "https://api.iptoasn.com/v1/as/ip/" + ip
            response = self.session.get(
                url,
                timeout=10,
            )
            if response.status_code == 200:
                return response.json()
            else:
                return {"error": "HTTP " + str(response.status_code)}
        except Exception as e:
            return {"error": str(e)}

    def search_certificates(
        self,
        domain: str,
    ) -> typing.Any:
        """
        Searches for SSL/TLS certificates related to the given domain using crt.sh.

        :param domain: The domain name to search certificates for.
        :return: A list of certificate entries in JSON format.
        """
        try:
            url: str = "https://crt.sh/?q=%25." + domain + "&output=json"
            response = self.session.get(
                url,
                timeout=10,
            )
            # Sometimes crt.sh returns an empty response
            return response.json() if response.text.strip() else []
        except Exception as e:
            return {"error": str(e)}

    def get_page_source(
        self,
        url: str,
    ) -> str:
        """
        Retrieves the full HTML source of the specified web page.

        :param url: The target URL.
        :return: The page source as a string.
        """
        try:
            response = self.session.get(
                url,
                timeout=10,
            )
            return response.text
        except Exception as e:
            return "Error: " + str(e)

    def reverse_ip_lookup(
        self,
        ip: str,
    ) -> typing.List[str]:
        """
        Performs a reverse IP lookup using a free public API. The API returns a list of domains
        hosted on the same IP address.

        :param ip: The IP address to query.
        :return: A list of domain names.
        """
        try:
            url: str = "https://api.hackertarget.com/reverseiplookup/?q=" + ip
            response = self.session.get(
                url,
                timeout=10,
            )
            content: str = response.text.strip()
            if "error" in content.lower() or content == "No records found":
                return []
            # Assume domains are returned as newline-separated values
            domains: typing.List[str] = content.splitlines()
            return domains
        except:
            return []

    def get_subdomains(
        self,
        domain: str,
    ) -> typing.List[str]:
        """
        Attempts to extract subdomains for the given domain by querying crt.sh and processing
        the certificate data.

        :param domain: The domain name to search for subdomains.
        :return: A list of unique subdomains.
        """
        subdomains: typing.Set[str] = set()
        certs: typing.Any = self.search_certificates(domain)
        if isinstance(
            certs,
            list,
        ):
            for cert in certs:
                if "name_value" in cert:
                    # Some certificates include multiple domains separated by newline characters.
                    names: typing.List[str] = cert["name_value"].splitlines()
                    for name in names:
                        # Only add names that contain the target domain.
                        if domain.lower() in name.lower():
                            subdomains.add(name.strip())
        return sorted(subdomains)

    def check_website_status(
        self,
        url: str,
    ) -> int:
        """
        Checks the HTTP status code for the given website.

        :param url: The target URL.
        :return: The HTTP status code (e.g., 200, 404).
        """
        try:
            response = self.session.get(
                url,
                timeout=10,
            )
            return response.status_code
        except Exception:
            return 0

    def check_http_redirects(
        self,
        url: str,
    ) -> typing.List[str]:
        """
        Follows HTTP redirects for the given URL and returns the redirection chain.

        :param url: The starting URL.
        :return: A list of URLs representing the redirect chain.
        """
        chain: typing.List[str] = []
        try:
            current_url: str = url
            for _ in range(10):
                response = self.session.get(
                    current_url,
                    timeout=10,
                    allow_redirects=False,
                )
                chain.append(current_url)
                if (
                    response.status_code
                    not in {
                        301,
                        302,
                        303,
                        307,
                        308,
                    }
                    or "Location" not in response.headers
                ):
                    break
                location: str = response.headers["Location"]
                current_url = (
                    location
                    if location.startswith("http")
                    else urllib.parse.urljoin(
                        chain[-1],
                        location,
                    )
                )
            return chain
        except Exception as e:
            return ["Error: " + str(e)]

    def get_page_load_time(
        self,
        url: str,
    ) -> typing.Any:
        """
        Measures and returns the page load time for the given URL.

        :param url: The target URL.
        :return: A dictionary with the URL and load time in seconds.
        """
        try:
            response = self.session.get(
                url,
                timeout=10,
            )
            elapsed: float = response.elapsed
            return {
                "url": url,
                "load_time_seconds": elapsed,
            }
        except Exception as e:
            return {"error": str(e)}

    def check_url_security_headers(
        self,
        url: str,
    ) -> typing.Any:
        """
        Checks for common security headers on the specified URL.

        :param url: The target URL.
        :return: A dictionary indicating the presence (True/False) of various security headers.
        """
        try:
            response = self.session.get(
                url,
                timeout=10,
            )
            headers: dict = dict(response.headers)
            security_headers: typing.Dict[
                str,
                bool,
            ] = {
                "Content-Security-Policy": "Content-Security-Policy"
                in headers,
                "Strict-Transport-Security": "Strict-Transport-Security"
                in headers,
                "X-Frame-Options": "X-Frame-Options" in headers,
                "X-Content-Type-Options": "X-Content-Type-Options" in headers,
            }
            return security_headers
        except Exception as e:
            return {"error": str(e)}

    def get_cookies_from_url(
        self,
        url: str,
    ) -> typing.Any:
        """
        Retrieves cookies set by the specified URL.

        :param url: The target URL.
        :return: A dictionary of cookies or an error.
        """
        try:
            response = self.session.get(
                url,
                timeout=10,
            )
            cookies: dict = response.cookies.get_dict()
            return cookies
        except Exception as e:
            return {"error": str(e)}

    def get_sitemap_urls(
        self,
        url: str,
    ) -> typing.Any:
        """
        Retrieves and parses the sitemap.xml at the given URL and returns a list of URLs found.

        :param url: The base URL of the website.
        :return: A list of URLs extracted from the sitemap, or an error.
        """
        try:
            sitemap_url: str = url.rstrip("/") + "/sitemap.xml"
            response = self.session.get(
                sitemap_url,
                timeout=10,
            )
            if response.status_code != 200:
                return {
                    "error": "Failed to retrieve sitemap, HTTP "
                    + str(response.status_code)
                }
            root: ElementTree.Element = ElementTree.fromstring(
                response.content
            )
            urls: typing.List[str] = [
                elem.text.strip()
                for elem in root.iter()
                if elem.tag.endswith("loc") and elem.text
            ]
            return urls
        except Exception as e:
            return {"error": str(e)}

    def check_social_media_username(
        self,
        username: str,
    ) -> typing.Dict[
        str,
        bool,
    ]:
        platforms: typing.Dict[
            str,
            str,
        ] = {
            "Twitter": "https://twitter.com/" + username,
            "Facebook": "https://www.facebook.com/" + username,
            "Instagram": "https://www.instagram.com/" + username,
            "LinkedIn": "https://www.linkedin.com/in/" + username,
            "GitHub": "https://github.com/" + username,
            "YouTube": "https://www.youtube.com/" + username,
        }
        results: typing.Dict[
            str,
            bool,
        ] = {}
        for (
            site,
            url,
        ) in platforms.items():  # Renamed 'platform' to 'site'
            try:
                response = self.session.head(
                    url,
                    timeout=10,
                )
                results[site] = response.status_code == 200
            except Exception:
                results[site] = False
        return results

    def get_sentence_embedding(
        self,
        text: str,
    ) -> typing.Any:
        """
        Returns a sentence embedding vector for the given text using the Universal
        Sentence Encoder from TensorFlow Hub.
        """
        try:
            if not hasattr(
                self,
                "use_model",
            ):
                # Load the Universal Sentence Encoder model only once.
                self.use_model = tensorflow_hub.KerasLayer(
                    "https://tfhub.dev/google/universal-sentence-encoder/4"
                )
            embedding = self.use_model([text])
            # Convert the TensorFlow tensor to a Python list.
            return embedding.numpy().tolist()
        except Exception as e:
            return {"error": str(e)}

    def perform_text_classification_tf(
        self,
        text: str,
    ) -> typing.Any:
        """
        Performs text classification using a pre-trained TensorFlow model.
        This is a stub implementation that loads a model from TensorFlow Hub (here using
        a neural network language model for embeddings) and then applies a dummy classification
        based on the sum of the embedding values.
        """
        try:
            if not hasattr(
                self,
                "tf_classification_model",
            ):
                # Load a text embedding model (this is just for demonstration purposes).
                self.tf_classification_model = tensorflow_hub.KerasLayer(
                    "https://tfhub.dev/google/nnlm-en-dim50/2"
                )
            embedding = self.tf_classification_model([text])
            # Dummy classification logic: sum the embedding values.
            score: float = float(tf.reduce_sum(embedding))
            label: str = "Positive" if score > 0 else "Negative"
            return {
                "score": score,
                "label": label,
            }
        except Exception as e:
            return {"error": str(e)}

    def generate_advanced_text_summary(
        self,
        text: str,
    ) -> typing.Any:
        """
        Generates an advanced summary of the provided text using a HuggingFace Transformers
        summarization pipeline. This produces a more nuanced and contextual summary.

        :param text: The input text to summarize.
        :return: A list containing the summary as returned by the pipeline, or an error.
        """
        try:
            if not hasattr(
                self,
                "hf_summarizer",
            ):
                self.hf_summarizer = transformers.pipeline("summarization")
            summary: typing.Any = self.hf_summarizer(
                text,
                max_length=150,
                min_length=40,
                do_sample=False,
            )
            return summary
        except Exception as e:
            return {"error": str(e)}

    def perform_advanced_named_entity_recognition(
        self,
        text: str,
    ) -> typing.Any:
        """
        Uses spaCy's advanced NER capabilities to extract named entities from text.

        :param text: The input text.
        :return: A list of tuples (entity_label, entity_text) or an error.
        """
        try:
            if not hasattr(
                self,
                "spacy_nlp",
            ):
                self.spacy_nlp = spacy.load("en_core_web_sm")
            doc: spacy.tokens.Doc = self.spacy_nlp(text)
            entities: typing.List[
                typing.Tuple[
                    str,
                    str,
                ]
            ] = [
                (
                    ent.label_,
                    ent.text,
                )
                for ent in doc.ents
            ]
            return entities
        except Exception as e:
            return {"error": str(e)}

    def perform_advanced_sentiment_analysis(
        self,
        text: str,
    ) -> typing.Any:
        """
        Uses HuggingFace Transformers to perform advanced sentiment analysis on text.

        :param text: The input text.
        :return: A list containing sentiment analysis results or an error.
        """
        try:
            if not hasattr(
                self,
                "hf_sentiment",
            ):
                self.hf_sentiment = transformers.pipeline("sentiment-analysis")
            result: typing.Any = self.hf_sentiment(text)
            return result
        except Exception as e:
            return {"error": str(e)}

    def perform_question_answering(
        self,
        context: str,
        question: str,
    ) -> typing.Dict[
        str,
        typing.Any,
    ]:
        """
        Uses a HuggingFace Transformers question-answering pipeline to answer a question
        based on the provided context.

        :param context: The context text.
        :param question: The question to answer.
        :return: A dictionary with the answer and score, or an error.
        """
        try:
            if not hasattr(
                self,
                "hf_qa",
            ):
                self.hf_qa = transformers.pipeline("question-answering")

            result: typing.Dict[
                str,
                typing.Any,
            ] = self.hf_qa(
                {
                    "question": question,
                    "context": context,
                }
            )  # type: ignore
            return result
        except Exception as e:
            return {"error": str(e)}

    def ocr_image_from_url(
        self,
        image_url: str,
    ) -> typing.Any:
        """
        Downloads an image from a URL and performs OCR on it using pytesseract.

        :param image_url: The URL of the image.
        :return: The extracted text from the image or an error.
        """
        try:
            response = self.session.get(
                image_url,
                timeout=10,
            )
            if response.status_code != 200:
                return {"error": "HTTP " + str(response.status_code)}
            image_bytes: bytes = response.content
            image_stream: io.BytesIO = io.BytesIO(image_bytes)
            image: PIL.Image.Image = PIL.Image.open(image_stream)
            ocr_text: str = pytesseract.pytesseract.image_to_string(image)
            return ocr_text
        except Exception as e:
            return {"error": str(e)}

    def build_entity_graph(
        self,
        text: str,
    ) -> typing.Any:
        # sourcery skip: class-extract-method
        """
        Builds a co-occurrence graph of named entities extracted from the provided text.
        The text is split into sentences, NER (using spaCy via advanced NER) extracts entities,
        and a NetworkX graph is built where an edge indicates co-occurrence in a sentence.
        The graph is drawn with Matplotlib and the resulting image is returned as a base64-encoded PNG.
        """
        try:
            # Split text into sentences.
            sentences: typing.List[str] = nltk.tokenize.sent_tokenize(text)
            # Initialize an empty undirected graph.
            g: networkx.Graph = networkx.Graph()

            for sentence in sentences:
                # Use the advanced NER method (spaCy-based) from the parent class.
                entities: typing.Any = (
                    self.perform_advanced_named_entity_recognition(sentence)
                )
                if isinstance(
                    entities,
                    list,
                ):
                    unique_entities: typing.List[str] = []
                    for ent in entities:
                        if (
                            isinstance(
                                ent,
                                tuple,
                            )
                            and len(ent) == 2
                        ):
                            entity_text: str = ent[1]
                            if entity_text not in unique_entities:
                                unique_entities.append(entity_text)
                    # Connect every pair of entities in the sentence.
                    for i in range(len(unique_entities)):
                        for j in range(
                            i + 1,
                            len(unique_entities),
                        ):
                            if g.has_edge(
                                unique_entities[i],
                                unique_entities[j],
                            ):
                                g[unique_entities[i]][unique_entities[j]][
                                    "weight"
                                ] += 1
                            else:
                                g.add_edge(
                                    unique_entities[i],
                                    unique_entities[j],
                                    weight=1,
                                )

            # Draw the graph using a spring layout.
            pos: typing.Any = networkx.spring_layout(g)
            matplotlib.pyplot.figure(
                figsize=(
                    10,
                    10,
                )
            )
            networkx.draw_networkx_nodes(
                g,
                pos,
                node_size=500,
                node_color="lightblue",
            )

            # Prepare edge widths as a list of floats.
            edge_widths: typing.List[float] = [
                float(g[u][v]["weight"]) for u, v in g.edges()
            ]
            # Use a type: ignore comment because type stubs expect a float, even though a list is allowed at runtime.
            networkx.draw_networkx_edges(
                g,
                pos,
                width=edge_widths,  # type: ignore
            )

            networkx.draw_networkx_labels(
                g,
                pos,
                font_size=10,
            )
            buf: io.BytesIO = io.BytesIO()
            matplotlib.pyplot.savefig(
                buf,
                format="png",
            )
            matplotlib.pyplot.close()
            buf.seek(0)
            encoded_image: str = base64.b64encode(buf.getvalue()).decode(
                "utf-8"
            )
            return {"graph_image_base64": encoded_image}
        except Exception as e:
            return {"error": str(e)}

    def perform_semantic_search(
        self,
        documents: typing.List[str],
        query: str,
    ) -> typing.Any:
        """
        Performs semantic search over a list of documents using SentenceTransformers.
        It computes embeddings for all documents and the query, then returns a list of tuples
        (document index, similarity score) sorted in descending order of similarity.
        """
        try:
            if not hasattr(
                self,
                "semantic_model",
            ):
                self.semantic_model = (
                    sentence_transformers.SentenceTransformer(
                        "all-MiniLM-L6-v2"
                    )
                )
            doc_embeddings: tf.python.framework.tensor.Tensor = (
                self.semantic_model.encode(
                    documents,
                    convert_to_tensor=True,
                )
            )
            query_embedding: tf.python.framework.tensor.Tensor = (
                self.semantic_model.encode(
                    [query],
                    convert_to_tensor=True,
                )
            )
            # Compute cosine similarities.
            similarities: numpy.ndarray = numpy.dot(
                doc_embeddings,
                query_embedding[0],
            ) / (
                numpy.linalg.norm(
                    doc_embeddings,
                    axis=1,
                )
                * numpy.linalg.norm(query_embedding)
            )
            scores: typing.List[
                typing.Tuple[
                    int,
                    float,
                ]
            ] = [
                (
                    i,
                    float(score),
                )
                for i, score in enumerate(similarities)
            ]
            scores.sort(
                key=lambda x: x[1],
                reverse=True,
            )
            return scores
        except Exception as e:
            return {"error": str(e)}

    def translate_text(
        self,
        text: str,
        target_language: str = "en",
    ) -> typing.Any:
        """
        Translates the provided text into the target language using a HuggingFace
        translation pipeline. The model is loaded on first use.
        """
        try:
            if not hasattr(
                self,
                "hf_translator",
            ):
                # The model name is constructed for translation from English to the target language.
                self.hf_translator = transformers.pipeline(
                    "translation",
                    model="Helsinki-NLP/opus-mt-en-{}".format(target_language),
                )
            translation: typing.Any = self.hf_translator(text)
            return translation
        except Exception as e:
            return {"error": str(e)}

    def extract_audio_transcript(
        self,
        audio_url: str,
    ) -> typing.Any:
        """
        Downloads an audio file from the specified URL and extracts its transcript
        using the SpeechRecognition library with the Sphinx recognizer.
        """
        try:
            response = self.session.get(
                audio_url,
                timeout=10,
            )
            if response.status_code != 200:
                return {"error": "HTTP " + str(response.status_code)}
            audio_bytes: bytes = response.content
            audio_file: io.BytesIO = io.BytesIO(audio_bytes)
            recognizer: speech_recognition.Recognizer = (
                speech_recognition.Recognizer()
            )
            # speech_recognition.AudioFile can accept a file-like object.
            with speech_recognition.AudioFile(audio_file) as source:
                audio_data: speech_recognition.AudioData = recognizer.record(
                    source
                )
            transcript: str = recognizer.recognize_vosk(audio_data)
            return transcript
        except Exception as e:
            return {"error": str(e)}

    def extract_urls_and_build_link_graph(
        self,
        text: str,
    ) -> typing.Any:
        """
        Extracts URLs from the provided text, then for each URL retrieves its page content
        and extracts further URLs. A directed graph is built where an edge from URL A to URL B
        indicates that page A contains a link to page B. The graph is visualized with NetworkX
        and Matplotlib and returned as a base64-encoded image.
        """
        try:
            urls: typing.List[str] = extract_urls_from_text(text)
            g: networkx.DiGraph = networkx.DiGraph()
            for url in urls:
                g.add_node(url)
                try:
                    response = self.session.get(
                        url,
                        timeout=10,
                    )
                    if response.status_code == 200:
                        page_text: str = response.text
                        linked_urls: typing.List[str] = extract_urls_from_text(
                            page_text
                        )
                        for lurl in linked_urls:
                            if lurl in urls:
                                g.add_edge(
                                    url,
                                    lurl,  # noqa: F841
                                )
                except Exception:
                    continue
            pos: typing.Any = networkx.spring_layout(g)
            matplotlib.pyplot.figure(
                figsize=(
                    12,
                    12,
                )
            )
            networkx.draw_networkx(
                g,
                pos,
                with_labels=True,
                node_size=500,
                font_size=8,
            )
            buf: io.BytesIO = io.BytesIO()
            matplotlib.pyplot.savefig(
                buf,
                format="png",
            )
            matplotlib.pyplot.close()
            buf.seek(0)
            encoded_image: str = base64.b64encode(buf.getvalue()).decode(
                "utf-8"
            )
            return {"link_graph_image_base64": encoded_image}
        except Exception as e:
            return {"error": str(e)}
