import re
import string
from functools import cache
from typing import Literal

import libcst
import nltk
from nltk.corpus import stopwords, words
from nltk.tokenize import word_tokenize

from optimizer.models import CodeExplanationAndID

for corpus in ["words", "stopwords", "punkt_tab"]:
    nltk.download(corpus)


mini_python_corpus = {"efficient", "updates", "json_schema_update", "handled", "single", "pass", "making", "process", "method", "extraction", "clarity", "extracted", "processing", "logic", "fieldinfo", "private", "_process_field_info", "separates", "concerns", "avoids", "duplicating", "optimized", "version", "provided", "code", "in-place", "loop", "assignments", "assignment","data", "string", "generator", "conditional", "eliminations", "optimizations", "early", "return", "define_expected_missing_refs", "function", "used", "allowed_missing_refs", "empty", "avoiding", "unnecessary", "memory", "efficiency", "keeping", "intermediate", "variables", "like", "refs", "sets", "ensures", "operations", "difference", "time", "memory-efficient", "especially", "larger", "collections","avoid", "checking", "key", "exists", "inserting", "streamlining", "insertion", "dictionary", "entries","set", "convert", "keys", "faster", "operation","list", "list-based", "optimize", "given", "python", "program", "better", "runtime", "performance", "usage", "set operations", "early return", "setdefault", "inserts", "eliminated", "redundancy", "removed", "duplicate", "definitionoptimize", "following", "uses", "improvements", "redundant", "calls", "combines", "schema", "efficiently", "Explanation", "explanation", "definition", "increased", "workflow", "call", "update", "duplicates", "duplicated", "consults", "eliminates", "membership", "credits", "environmenterror", "generated", "hash", "copyright", "divmod", "range", "eval", "reversed", "syntaxwarning", "merged", "floatingpointerror", "syntaxerror", "needed", "typeerror", "anext", "processed", "quit", "stopiteration", "overhead", "float", "__build_class__", "oct", "neccesary", "processes", "runtimewarning", "passed", "versioning", "all", "focus", "complexity", "mainly", "connectionreseterror", "keyboardinterrupt", "max", "versioned", "modulenotfounderror", "delattr", "decoding", "reduced", "lookup", "management", "conditions", "importwarning", "merging", "versions", "__debug__", "lookuperror", "exec", "self", "blockingioerror", "unicodetranslateerror", "is", "processlookuperror", "except", "import", "inefficiencies", "getattr", "id", "changed", "check", "fileexistserror", "catched", "runtimeerror", "raise", "nonlocal", "property", "taberror", "for", "baseexceptiongroup", "del", "frozenset", "setattr", "type", "pow", "warning", "zerodivisionerror", "assertionerror", "optimization", "assert", "true", "managing", "attributeerror", "multiple", "input", "try-except", "use", "correctness", "int", "exception", "unicodewarning", "catching", "futurewarning", "license", "vars", "merges", "yield", "slice", "results", "nameerror", "recursionerror", "unpacking", "block", "bool", "help", "lambda", "interruptederror", "content", "open", "or", "speed", "overflowerror", "classmethod", "resourcewarning", "changes", "exceptiongroup", "improvement", "oserror", "enumerate", "encodingwarning", "hex", "await", "isadirectoryerror", "output", "improves", "callable", "dict", "lookups", "combined", "indexerror", "next", "while", "iteration", "staticmethod", "connectionerror", "with", "average", "response", "childprocesserror", "provides", "json", "converted", "continue", "deprecationwarning", "generatorexit", "userwarning", "responses", "compile", "structures", "globals", "issubclass", "lists", "__doc__", "unicodeencodeerror", "necessary", "milliseconds", "unicodeerror", "arithmeticerror", "average-time", "signatures", "params", "global", "complex", "connectionrefusederror", "object", "baseexception", "optimizes", "improved", "not", "performed", "elif", "iterations", "hasattr", "regression", "def", "certainly", "checked", "bytes", "as", "encompass", "sorted", "break", "try", "zip", "checks", "in", "timeouterror", "byteswarning", "details", "class", "format", "nested", "creating", "ensuring", "possible", "__name__", "ascii", "dir", "condition", "filenotfounderror", "len", "__peg_parser__", "super", "else", "filter", "tests", "ord", "stopasynciteration", "indentationerror", "if", "chr", "manages", "map", "any", "valueerror", "notadirectoryerror", "importerror", "unboundlocalerror", "reduces", "__loader__", "notimplementederror", "min", "result", "connectionabortederror", "locals", "round", "smallest", "systemexit", "from", "ellipsis", "none", "unicodedecodeerror", "dictionaries", "exit", "iter", "memoryview", "brokenpipeerror", "breakpoint", "__import__", "evaluated", "exceptions", "and", "existing", "ensure", "aiter", "keyerror", "sum", "pendingdeprecationwarning", "abs", "eoferror", "finally", "bytearray", "print", "windowserror", "minimal", "catch", "false", "async", "__spec__", "permissionerror", "structure", "bin", "repr", "str", "tuple", "__package__", "buffererror", "systemerror", "isinstance", "referenceerror", "memoryerror", "notimplemented", "ioerror", "algorithm", "direct", "comparison", "using", "type_", "__class__", "performs"}  # fmt: skip


class CodeVisitor(libcst.CSTVisitor):
    def __init__(self, libcs_module: libcst.Module):
        self.code_info: set[str] = set()
        self.src = libcs_module.code

    def visit_Import(self, node: libcst.Import) -> None:
        for alias in node.names:
            if isinstance(alias.name.value, libcst.Name):
                self.code_info.add(alias.name.value.value)
            else:
                self.code_info.add(str(alias.name.value))

    def visit_Name(self, node: libcst.Name) -> None:
        self.code_info.add(node.value)

    def visit_Assign(self, node: libcst.Assign) -> None:
        for target in node.targets:
            if isinstance(target.target, libcst.Name):
                self.code_info.add(target.target.value)
            elif isinstance(target.target, libcst.Attribute):
                self.code_info.add(target.target.attr.value)

    def visit_FunctionDef(self, node: libcst.FunctionDef) -> None:
        self.code_info.add(node.name.value)


@cache
def get_nltk_words() -> set[str]:
    return set(words.words("en-basic")).union(mini_python_corpus)


@cache
def get_nltk_stopwords() -> set[str]:
    words = set(stopwords.words("english"))
    words.update(set(["`", ",", ".", ":", ";", "'", '"', "(", ")", "[", "]", "{", "}", "*"]))
    return words


class GibberishFilter:
    _simple_nonsense_re = re.compile(
        # Lack of any of the first 10 most-used letters in English.
        # (Reference: https://en.wikipedia.org/wiki/Letter_frequency on 2017-12-03)
        # This slightly improves detection of random strings.
        r"\A[^eariotnslcu]+\Z"
        # Repeated single characters: 5 or more in row.
        r"|(.)\1{5,}"
        # Repeating sequences.
        r"|(.)\2{2,}(.)\3{2,}"
        r"|(.)(.)\4\5\4\5"
        r"|(.)(.)(.)\6\7\8\6\7\8\6\7\8"
        # Subsequences of the alphabet.  There must be a more elegant way to test
        # for this but I haven't been able to think of it.
        r"|abcdef"
        r"|bcdefg"
        r"|cdefgh"
        r"|defghi"
        r"|efghij"
        r"|fghijk"
        r"|ghijkl"
        r"|hijklm"
        r"|ijklmn"
        r"|jklmno"
        r"|klmnop"
        r"|lmnopq"
        r"|mnopqr"
        r"|nopqrs"
        r"|opqrst"
        r"|pqrstu"
        r"|qrstuv"
        r"|rstuvw"
        r"|stuvwx"
        r"|tuvwxy"
        r"|uvwxyz"
        # Banging on a qwerty keyboard.
        r"|[asdfjkl]{8}",
        re.IGNORECASE,
    )
    _roman_numeral_re = re.compile(
        r"(^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$)", re.IGNORECASE
    )

    _nonalpha = string.punctuation + string.whitespace + string.digits
    _delete_nonalpha = str.maketrans("", "", _nonalpha)

    good_corpus = get_nltk_words()

    def __init__(self, CE: CodeExplanationAndID, threshold: float = 0.5):
        self.code_explanation = CE
        self.local_corpus: set[str] = self.good_corpus.copy()
        self.local_corpus.update(self.source_code_to_corpus())
        self.explanation_tokens: set[str] = set(word_tokenize(CE.explanation))
        self.threshold = threshold
        self.removed_words: set[str] = set()

    def filter_out(self, text: str) -> bool:
        if self._simple_nonsense_re.search(text) or self._roman_numeral_re.search(text):
            return True
        return False

    def sanitize_string(self, text: str, aggresive: bool = False) -> Literal[False] | str:
        text = text.strip()

        if not text or self.filter_out(text):
            return False

        if aggresive:
            text = text.translate(self._delete_nonalpha)

        return text.encode("ascii", errors="ignore").decode().lower()

    def source_code_to_corpus(self) -> set[str]:
        visitor = CodeVisitor(self.code_explanation.cst_module)
        self.code_explanation.cst_module.visit(visitor)
        return visitor.code_info

    def is_gibberish(self) -> bool:
        if len(self.explanation_tokens) == 0:
            return True
        valid_word_count = 0
        for token in self.explanation_tokens:
            if token.lower() in self.local_corpus:
                valid_word_count += 1
        if valid_word_count / len(self.explanation_tokens) < self.threshold:
            return True
        return False

    def _is_line_gibberish(self, text: str, prog_terms: bool = False) -> bool:
        if text.startswith("### "):
            text = text.removeprefix("### ")

        tokens = word_tokenize(text.lower())
        tokens = [token for token in tokens if token not in get_nltk_stopwords()]
        if len(tokens) == 1:
            return True
        if not tokens:
            return True

        if prog_terms:
            stop_words = get_nltk_stopwords()
            tokens = [token for token in tokens if token not in stop_words]
            valid_word_count = sum(1 for token in tokens if token in mini_python_corpus)
        else:
            valid_word_count = sum(1 for token in tokens if token in self.local_corpus)

        threshold = 0.60 if prog_terms else self.threshold
        return (valid_word_count / len(tokens)) < threshold

    def first_pass_explanation(self) -> str:
        new_explanation = []
        lines = self.code_explanation.explanation.split("\n")
        for line in lines:
            sanitized_line = self.sanitize_string(line, aggresive=False)
            if not sanitized_line:
                continue
            if self._is_line_gibberish(line):
                continue
            if self._is_line_gibberish(sanitized_line):
                continue
            new_explanation.append(line.strip())
        return "\n".join(new_explanation)

    def second_pass_explanation(self) -> str:
        lines = self.first_pass_explanation().split("\n")
        filtered_lines = [
            line.strip()
            for line in lines
            if self.sanitize_string(line, aggresive=True)
            and not self._is_line_gibberish(line, prog_terms=True)
            and len(word_tokenize(line)) > 1
        ]
        return "\n\n".join(filtered_lines)

    def cleanup_explanation(self) -> str:
        return self.second_pass_explanation()
