import base64
import difflib
import hashlib
import heapq
import json
import pathlib
import re
import typing
import unicodedata
import urllib.parse

import dateparser
import nltk
import numpy as np
import phonenumbers
import PyPDF2
import spacy
from crypto.Cipher import AES
from langdetect import detect
from nltk.sentiment import SentimentIntensityAnalyzer
from textblob import TextBlob


class TextUtils:
    def __init__(
        self,
    ):
        # Load spaCy model for Named Entity Recognition (NER)
        self.nlp = spacy.load("en_core_web_sm")
        self.sia = SentimentIntensityAnalyzer()
        # Download NLTK data (used for stopwords)
        nltk.download("punkt")
        nltk.download("stopwords")
        nltk.download("vader_lexicon")

    def detect_text_similarity(
        self,
        text1: str,
        text2: str,
    ) -> float:
        """Detect similarity between two pieces of text using Ratcliff/Obershelp pattern recognition."""
        sequence_matcher = difflib.SequenceMatcher(
            None,
            text1,
            text2,
        )
        return (
            sequence_matcher.ratio()
        )  # Returns similarity score between 0 and 1

    def detect_text_obfuscation(
        self,
        text: str,
    ) -> bool:
        """Detect common signs of obfuscated or hidden text."""
        suspicious_patterns = [
            r"\s{2,}",  # Multiple spaces
            r"[^\w\s]",  # Non-alphanumeric characters
            r"\d{6,}",  # Long sequences of digits
        ]

        return any(
            re.search(
                pattern,
                text,
            )
            for pattern in suspicious_patterns
        )

    def detect_language(
        self,
        text: str,
    ) -> str:
        """Detect the language of a given text."""
        try:
            return detect(text)
        except Exception as e:
            return str(e)

    def analyze_sentiment(
        self,
        text: str,
    ) -> str:
        """Analyze sentiment of the given text using TextBlob."""
        analysis = TextBlob(text)
        polarity = analysis.sentiment.polarity  # type: ignore
        if polarity > 0:
            return "Positive"
        elif polarity < 0:
            return "Negative"
        else:
            return "Neutral"

    def detect_text_changes(
        self,
        original_text: str,
        new_text: str,
    ) -> bool:
        """Compare two versions of text to see if any changes have been made."""
        original_hash = hashlib.sha256(original_text.encode()).hexdigest()
        new_hash = hashlib.sha256(new_text.encode()).hexdigest()
        return original_hash != new_hash  # Returns True if there is a change

    def detect_text_watermark(
        self,
        text: str,
    ) -> bool:
        """Detect if a watermark or hidden information is embedded within the text."""
        watermark_pattern = r"(\S{4,})(?=\s+\S{4,})"
        matches = re.findall(
            watermark_pattern,
            text,
        )
        return len(matches) > 0  # Return True if any watermark is found

    def detect_encoded_text(
        self,
        text: str,
    ) -> bool:
        """Detect if the text is encoded in base64 or another common encoding scheme."""
        try:
            base64.b64decode(text)
            return True  # If it can be decoded from base64, it may be encoded
        except Exception:
            return False

    def extract_patterns(
        self,
        text: str,
        pattern: str,
    ) -> typing.List[str]:
        """Extract specific patterns (like email addresses, phone numbers) from text."""
        return re.findall(
            pattern,
            text,
        )

    def encrypt_text(
        self,
        text: str,
        key: str,
    ) -> str:
        """Encrypt the text using AES encryption."""
        cipher = AES.new(
            key.encode(),
            AES.MODE_EAX,
        )
        (
            ciphertext,
            tag,
        ) = cipher.encrypt_and_digest(text.encode())
        return base64.b64encode(cipher.nonce + tag + ciphertext).decode()

    def decrypt_text(
        self,
        encrypted_text: str,
        key: str,
    ) -> str:
        """Decrypt the text using AES encryption."""
        data = base64.b64decode(encrypted_text.encode())
        (
            nonce,
            tag,
            ciphertext,
        ) = (
            data[:16],
            data[16:32],
            data[32:],
        )
        cipher = AES.new(
            key.encode(),
            AES.MODE_EAX,
            nonce=nonce,
        )
        plaintext = cipher.decrypt_and_verify(
            ciphertext,
            tag,
        )
        return plaintext.decode()

    def extract_text_from_pdf(
        self,
        pdf_path: pathlib.Path,
    ) -> str:
        """Extract text from a PDF document."""
        with open(
            pdf_path,
            "rb",
        ) as file:
            reader = PyPDF2.PdfReader(file)
            return "".join(page.extract_text() for page in reader.pages)

    def check_for_fake_news(
        self,
        url: str,
    ) -> bool:
        """Check if a URL is potentially fake news or suspicious using a public open-source tool."""
        # Example: Using "NewsGuard" or another free source, but here we simulate by checking for known fake sites
        fake_news_sites = [
            "example-fake-news.com",
            "another-fake-news.org",
        ]
        parsed_url = urllib.parse.urlparse(url)
        return parsed_url.netloc in fake_news_sites

    def extract_social_media_handles(
        self,
        text: str,
    ) -> typing.List[str]:
        """Extract social media handles (e.g., Twitter, Instagram) from the text."""
        social_media_patterns = [
            r"@([A-Za-z0-9_]+)",  # Twitter and Instagram handles
        ]
        handles = []
        for pattern in social_media_patterns:
            handles.extend(
                re.findall(
                    pattern,
                    text,
                )
            )
        return handles

    def extract_json_data(
        self,
        text: str,
    ) -> typing.Optional[dict]:
        """Attempt to extract JSON data embedded in the text."""
        try:
            return json.loads(text)
        except json.JSONDecodeError:
            return None

    def extract_links(
        self,
        text: str,
    ) -> typing.List[str]:
        """Extract URLs or links from the text."""
        url_pattern = r"(https?://[^\s]+)"
        return re.findall(
            url_pattern,
            text,
        )

    def check_ip_address_in_text(
        self,
        text: str,
    ) -> typing.List[str]:
        """Extract and check IP addresses embedded in text."""
        ip_pattern = r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b"
        return re.findall(
            ip_pattern,
            text,
        )

    def analyze_text_with_tfidf(
        self,
        corpus: typing.List[str],
    ) -> typing.List[str]:
        """Perform TF-IDF analysis to identify important keywords in a set of documents."""
        from sklearn.feature_extraction.text import TfidfVectorizer

        vectorizer = TfidfVectorizer(stop_words="english")
        tfidf_matrix = vectorizer.fit_transform(corpus)
        feature_names = vectorizer.get_feature_names_out()

        # Find the most important terms for each document
        important_terms = []
        for doc_idx in range(tfidf_matrix.shape[0]):
            scores = zip(
                feature_names,
                tfidf_matrix[doc_idx].toarray()[0],  # type: ignore
            )
            important_terms.append(
                sorted(
                    scores,
                    key=lambda x: -x[1],
                )[:5]
            )  # Top 5 terms
        return important_terms

    def detect_plagiarism(
        self,
        text: str,
        corpus: typing.List[str],
    ) -> bool:
        """Detect if the text is similar to any document in a given corpus."""
        return any(
            self.detect_text_similarity(
                text,
                doc,
            )
            > 0.8
            for doc in corpus
        )

    def detect_profanity(
        self,
        text: str,
    ) -> bool:
        """Detect offensive language in the text using NLTK."""
        from nltk.corpus import stopwords

        # Example: using NLTK's stopwords list as a placeholder for a profanity list
        profanity_list = stopwords.words("english")

        return any(bad_word in text.lower() for bad_word in profanity_list)

    def extract_text_from_image(
        self,
        image_path: pathlib.Path,
    ) -> typing.Optional[str]:
        """Extract text from an image using OCR (Optical Character Recognition)."""
        import pytesseract
        from PIL import Image

        try:
            image = Image.open(image_path)
            text = pytesseract.pytesseract.image_to_string(image)
            return text.strip() if text else None
        except Exception:
            return None

    def summarize_text(
        self,
        text: str,
        ratio: float = 0.2,
    ) -> str:
        """Generate an extractive summary of the text using TextRank algorithm."""
        from sumy.nlp.tokenizers import Tokenizer
        from sumy.parsers.plaintext import PlaintextParser
        from sumy.summarizers.text_rank import TextRankSummarizer

        parser = PlaintextParser.from_string(
            text,
            Tokenizer("english"),
        )
        summarizer = TextRankSummarizer()
        summary = summarizer(
            parser.document,
            sentences_count=int(len(parser.document.sentences) * ratio),
        )
        return " ".join([str(sentence) for sentence in summary])

    def translate_text(
        self,
        text: str,
        target_lang: str = "en",
    ) -> typing.Optional[str]:
        """Translate text to target language using googletrans."""
        from googletrans import Translator

        try:
            translator = Translator()
            result = translator.translate(
                text,
                dest=target_lang,
            )
            return result.text if result else None  # type: ignore
        except Exception:
            return None

    def calculate_readability(
        self,
        text: str,
    ) -> typing.Dict[
        str,
        float,
    ]:
        """Calculate text readability scores (Flesch-Kincaid, SMOG, etc.)."""
        import textstat

        return {
            "flesch_reading_ease": textstat.textstat.flesch_reading_ease(text),
            "smog_index": textstat.textstat.smog_index(text),
            "coleman_liau_index": textstat.textstat.coleman_liau_index(text),
            "automated_readability_index": textstat.textstat.automated_readability_index(
                text
            ),
        }

    def extract_keyphrases(
        self,
        text: str,
        top_n: int = 5,
    ) -> typing.List[str]:
        """Extract keyphrases using RAKE (Rapid Automatic Keyword Extraction)."""
        from rake_nltk import Rake

        r = Rake()
        r.extract_keywords_from_text(text)
        return r.get_ranked_phrases()[:top_n]

    def normalize_text(
        self,
        text: str,
    ) -> str:
        """Normalize text by removing diacritics, lowercasing, and cleaning whitespace."""
        text = (
            unicodedata.normalize(
                "NFKD",
                text,
            )
            .encode(
                "ascii",
                "ignore",
            )
            .decode("utf-8")
        )
        return "".join(text.lower().strip().split())

    def generate_ngrams(
        self,
        text: str,
        n: int = 2,
    ) -> typing.List[
        typing.Tuple[
            str,
            ...,
        ]
    ]:
        """Generate character or word n-grams from text."""
        tokens = text.split()
        return [tuple(tokens[i : i + n]) for i in range(len(tokens) - n + 1)]

    def pos_tagging(
        self,
        text: str,
    ) -> typing.List[
        typing.Tuple[
            str,
            str,
        ]
    ]:
        """Perform part-of-speech tagging using spaCy."""
        doc = self.nlp(text)
        return [
            (
                token.text,
                token.pos_,
            )
            for token in doc
        ]

    def handle_emojis(
        self,
        text: str,
        action: str = "remove",
    ) -> str:
        """Handle emojis in text (remove or analyze)."""
        import emoji

        if action == "remove":
            return emoji.replace_emoji(
                text,
                replace="",
            )
        return emoji.demojize(text)

    def expand_contractions(
        self,
        text: str,
    ) -> str:
        """Expand English contractions in text."""
        from contractions import fix

        return fix(text)  # type: ignore

    def back_translate(
        self,
        text: str,
        intermediate_lang: str = "fr",
    ) -> typing.Optional[str]:
        """Generate paraphrased text using back-translation."""
        intermediate = self.translate_text(
            text,
            intermediate_lang,
        )
        return (
            self.translate_text(
                intermediate,
                "en",
            )
            if intermediate
            else None
        )

    def anonymize_text(
        self,
        text: str,
    ) -> str:
        """Anonymize text by replacing named entities with placeholders."""
        doc = self.nlp(text)
        anonymized = []
        for token in doc:
            if token.ent_type_:
                anonymized.append(f"[{token.ent_type_}]")
            else:
                anonymized.append(token.text)
        return " ".join(anonymized)

    def detect_encoding_issues(
        self,
        text: str,
    ) -> bool:
        """Detect encoding problems like mojibake."""
        from ftfy import fix_text

        return fix_text(text) != text

    def text_deduplication(
        self,
        texts: typing.List[str],
    ) -> typing.List[str]:
        """Remove duplicate texts using hashing."""
        seen = set()
        return [x for x in texts if not (hash(x) in seen or seen.add(hash(x)))]

    def generate_checksum(
        self,
        text: str,
    ) -> str:
        """Generate a SHA-256 checksum for text content."""
        return hashlib.sha256(text.encode()).hexdigest()

    def analyze_text_compliance(
        self,
        text: str,
    ) -> typing.Dict[
        str,
        bool,
    ]:
        """Check text for compliance patterns (GDPR, PCI, etc.)."""
        patterns = {
            "credit_card": r"\b(?:\d[ -]*?){13,16}\b",
            "ssn": r"\b\d{3}-\d{2}-\d{4}\b",
            "phone_number": r"\b(?:\+?1-?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b",
        }
        return {
            k: bool(
                re.search(
                    v,
                    text,
                )
            )
            for k, v in patterns.items()
        }

    def calculate_semantic_similarity(
        self,
        text1: str,
        text2: str,
    ) -> float:
        """Calculate semantic similarity using sentence embeddings."""
        from sentence_transformers import SentenceTransformer

        model = SentenceTransformer("all-MiniLM-L6-v2")
        embeddings = model.encode(
            [
                text1,
                text2,
            ]
        )
        return float(
            np.dot(
                embeddings[0],
                embeddings[1],
            )
        )

    def generate_word_cloud(
        self,
        text: str,
        output_path: pathlib.Path,
    ) -> None:
        """Generate and save a word cloud image from text."""
        from wordcloud import WordCloud

        wc = WordCloud(
            width=800,
            height=400,
        ).generate(text)
        wc.to_file(str(output_path))

    def detect_tonality(
        self,
        text: str,
    ) -> typing.Dict[
        str,
        float,
    ]:
        """Analyze text tonality using LIWC-style categories."""
        positive_words = set(nltk.corpus.opinion_lexicon.positive())
        negative_words = set(nltk.corpus.opinion_lexicon.negative())
        words = text.lower().split()
        pos_count = sum(word in positive_words for word in words)
        neg_count = sum(word in negative_words for word in words)
        total = len(words) or 1
        return {
            "positive": pos_count / total,
            "negative": neg_count / total,
            "neutral": (total - pos_count - neg_count) / total,
        }

    def create_text_fingerprint(
        self,
        text: str,
        ngram_size: int = 5,
    ) -> str:
        """Create a fingerprint for text using minhash algorithm."""
        minhash: typing.List[int] = []
        for i in range(len(text) - ngram_size + 1):
            ngram = text[i : i + ngram_size]
            minhash.append(hash(ngram))
        heapq.heapify(minhash)
        return "".join(
            f"{h:x}"
            for h in heapq.nsmallest(
                10,
                minhash,
            )
        )

    def extract_temporal_references(
        self,
        text: str,
    ) -> typing.List[
        typing.Dict[
            str,
            str,
        ]
    ]:
        """
        Extract and parse temporal references (dates, times, time zones) from text.
        Returns a list of dictionaries with parsed datetime objects and their context.
        """
        doc = self.nlp(text)
        temporal_data = []
        for ent in doc.ents:
            if ent.label_ in [
                "DATE",
                "TIME",
            ]:
                parsed_date = dateparser.parse(ent.text)
                if parsed_date:
                    temporal_data.append(
                        {
                            "text": ent.text,
                            "parsed_date": parsed_date.isoformat(),
                            "context": ent.sent.text.strip(),
                        }
                    )
        return temporal_data

    def detect_geolocation_references(
        self,
        text: str,
    ) -> typing.List[
        typing.Dict[
            str,
            str,
        ]
    ]:
        """Detect and extract geolocation references (cities, countries, landmarks) from text."""
        doc = self.nlp(text)
        locations = []
        locations.extend(
            {
                "location": ent.text,
                "context": ent.sent.text.strip(),
            }
            for ent in doc.ents
            if ent.label_
            in [
                "GPE",
                "LOC",
            ]
        )
        return locations

    def extract_phone_numbers(
        self,
        text: str,
    ) -> typing.List[
        typing.Dict[
            str,
            str,
        ]
    ]:
        """Extract and validate phone numbers from text using the `phonenumbers` library."""
        phone_numbers = []
        for match in phonenumbers.PhoneNumberMatcher(
            text,
            None,
        ):
            number = phonenumbers.format_number(
                match.number,
                phonenumbers.PhoneNumberFormat.E164,
            )
            phone_numbers.append(
                {
                    "number": number,
                    "context": text[match.start - 20 : match.end + 20].strip(),
                }
            )
        return phone_numbers

    def detect_crypto_wallet_addresses(
        self,
        text: str,
    ) -> typing.List[str]:
        """Detect cryptocurrency wallet addresses (Bitcoin, Ethereum, etc.) in text."""
        crypto_patterns = {
            "bitcoin": r"\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b",
            "ethereum": r"\b0x[a-fA-F0-9]{40}\b",
        }
        addresses = []
        for pattern in crypto_patterns.values():
            addresses.extend(
                re.findall(
                    pattern,
                    text,
                )
            )
        return addresses

    def extract_email_addresses(
        self,
        text: str,
    ) -> typing.List[str]:
        """Extract email addresses from text using regex."""
        email_pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
        return re.findall(
            email_pattern,
            text,
        )

    def analyze_sentiment_vader(
        self,
        text: str,
    ) -> typing.Dict[
        str,
        float,
    ]:
        """Perform sentiment analysis using NLTK's VADER (Valence Aware Dictionary and sEntiment Reasoner)."""
        sentiment_scores = self.sia.polarity_scores(text)
        return {
            "positive": sentiment_scores["pos"],
            "negative": sentiment_scores["neg"],
            "neutral": sentiment_scores["neu"],
            "compound": sentiment_scores["compound"],
        }

    def extract_hashtags(
        self,
        text: str,
    ) -> typing.List[str]:
        """Extract hashtags from social media-style text."""
        hashtag_pattern = r"#\w+"
        return re.findall(
            hashtag_pattern,
            text,
        )

    def extract_mentions(
        self,
        text: str,
    ) -> typing.List[str]:
        """Extract user mentions (e.g., @username) from text."""
        mention_pattern = r"@\w+"
        return re.findall(
            mention_pattern,
            text,
        )

    def detect_keywords(
        self,
        text: str,
        keywords: typing.List[str],
    ) -> typing.Dict[
        str,
        int,
    ]:
        """Detect and count occurrences of specific keywords in text."""
        text_lower = text.lower()
        return {
            keyword: text_lower.count(keyword.lower()) for keyword in keywords
        }

    def extract_ips(
        self,
        text: str,
    ) -> typing.List[str]:
        """Extract IP addresses (IPv4 and IPv6) from text."""
        ipv4_pattern = r"\b(?:\d{1,3}\.){3}\d{1,3}\b"
        ipv6_pattern = r"\b(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}\b"
        return re.findall(
            ipv4_pattern,
            text,
        ) + re.findall(
            ipv6_pattern,
            text,
        )

    def extract_urls(
        self,
        text: str,
    ) -> typing.List[str]:
        """Extract URLs from text."""
        url_pattern = r"https?://[^\s]+"
        return re.findall(
            url_pattern,
            text,
        )

    def detect_osint_indicators(
        self,
        text: str,
    ) -> typing.Dict[
        str,
        typing.List[str],
    ]:
        """Detect common OSINT indicators in text (emails, IPs, URLs, etc.)."""
        return {
            "emails": self.extract_email_addresses(text),
            "ips": self.extract_ips(text),
            "urls": self.extract_urls(text),
            "phone_numbers": [
                x["number"] for x in self.extract_phone_numbers(text)
            ],
            "crypto_addresses": self.detect_crypto_wallet_addresses(text),
        }

    def analyze_text_complexity(
        self,
        text: str,
    ) -> typing.Dict[
        str,
        float,
    ]:
        """Analyze text complexity using metrics like Flesch-Kincaid readability score."""
        import textstat

        return {
            "flesch_reading_ease": textstat.textstat.flesch_reading_ease(text),
            "word_count": len(text.split()),
            "sentence_count": len(
                re.findall(
                    r"[.!?]",
                    text,
                )
            ),
        }

    def extract_named_entities(
        self,
        text: str,
    ) -> typing.Dict[
        str,
        typing.List[str],
    ]:
        """Extract named entities (people, organizations, locations) using spaCy."""
        doc = self.nlp(text)
        entities = {
            "PERSON": [],
            "ORG": [],
            "GPE": [],
        }
        for ent in doc.ents:
            if ent.label_ in entities:
                entities[ent.label_].append(ent.text)
        return entities

    def detect_slang_or_jargon(
        self,
        text: str,
    ) -> typing.List[str]:
        """Detect slang words using NLTK's slang dictionary."""
        text_lower = text.lower()
        slang_list = {word.lower() for word in nltk.corpus.slang.words()}
        return [term for term in slang_list if term in text_lower]

    def extract_metadata_from_text(
        self,
        text: str,
    ) -> typing.Dict[
        str,
        typing.Any,
    ]:
        """Extract metadata-like information from text (language, sentiment, entities, etc.)."""
        return {
            "language": self.detect_language(text),
            "sentiment": self.analyze_sentiment_vader(text),
            "entities": self.extract_named_entities(text),
            "osint_indicators": self.detect_osint_indicators(text),
        }
