# --- Query Parser: Field Weight Extraction w/ Spelling Correction (Student Version) ---

import json
import logging
from typing import Dict, Tuple, Any, Optional, List
import re
import time

# Configure consistent logging with project standards
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("query_parser.log"),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

class QueryParser:
    """
    Parses user queries to extract field weights and search terms.

    Assignment note:
    Implement ONLY the TODO parts. Do NOT change method names/signatures.
    """

    def __init__(self,
                 allowed_fields: Optional[List[str]] = None,
                 default_weight: float = 1.0,
                 inverted_index_path: str = "inverted_index.json") -> None:
        # Default fields (title, summaries, genres, stars)
        self.allowed_fields = allowed_fields or ["title", "summaries", "genres", "stars"]
        self.default_weight = default_weight

        # Field aliases
        self.field_aliases = {"summary": "summaries"}

        # Default weights (copied on each parse)
        self.default_weights = {field: self.default_weight for field in self.allowed_fields}

        # Regex for field:weight
        self.field_weight_pattern = re.compile(r'^([a-zA-Z_]+):(-?[0-9]*\.?[0-9]+)$')

        # Vocab + stats for spelling correction
        self.vocabulary: set = set()
        self.term_df: Dict[str, int] = {}
        self.total_docs: int = 1
        self._load_vocabulary(inverted_index_path)

        logger.debug(f"QueryParser initialized with fields: {', '.join(self.allowed_fields)}")
        logger.debug(f"Vocabulary size: {len(self.vocabulary)} terms; total_docs={self.total_docs}")

    # ---------------- PROVIDED (keep) ----------------
    def _load_vocabulary(self, inverted_index_path: str) -> None:
        """Populate self.vocabulary (set), self.term_df (token->df), self.total_docs (int)."""
        try:
            with open(inverted_index_path, 'r', encoding='utf-8') as f:
                idx = json.load(f)
            index = idx.get("index", {})
            self.vocabulary = set(index.keys())
            self.term_df = {
                tok: (data.get("df", 0) if isinstance(data, dict) else 0)
                for tok, data in index.items()
            }
            meta = idx.get("metadata", {})
            self.total_docs = int(meta.get("total_documents", 1)) or 1
            logger.info(f"Vocabulary loaded: {len(self.vocabulary)} terms; total_docs={self.total_docs}")
        except Exception as e:
            logger.error(f"Failed to load vocabulary: {str(e)}")
            logger.warning("Spelling correction will degrade (no DF stats)")
            self.vocabulary = set()
            self.term_df = {}
            self.total_docs = 1

    # ---------------- STUDENT TODO ----------------
    def _morph_variants(self, term: str) -> List[str]:
        """
        Produce simple plural/singular variants of `term` and return only those present in `self.vocabulary`.

        Examples (not exhaustive):
        - dogs -> dog
        - boxes -> box
        - stories -> story
        - baby -> babies
        - bus -> buses

        Return:
            List[str]: variants found in vocabulary (may be empty).
        """
        # --------- TODO: implement morphological variant generation ---------
        raise NotImplementedError("TODO: implement _morph_variants")

    # ---------------- STUDENT TODO ----------------
    def _plural_class(self, w: str) -> str:
        """
        Classify plural 'shape' of a word for tie-breaking:
        - return one of {"IES","ES","S","NONE"} based on suffix patterns.
        """
        # --------- TODO: implement plural class detection ---------
        raise NotImplementedError("TODO: implement _plural_class")

    # ---------------- STUDENT TODO ----------------
    def _plural_base(self, w: str) -> str:
        """
        Roughly map plural forms back to a singular base:
        - stories -> story
        - boxes  -> box
        - dogs   -> dog
        Return "" if no plausible base.
        """
        # --------- TODO: implement plural base extraction ---------
        raise NotImplementedError("TODO: implement _plural_base")

    # ---------------- STUDENT TODO ----------------
    def _is_plausible_plural_of_vocab(self, w: str) -> bool:
        """
        Return True if `w` looks like a plural form AND its singular base is in vocabulary.
        This helps avoid changing user input like 'rings' when 'ring' exists.
        """
        # --------- TODO: implement plural plausibility check using _plural_base and self.vocabulary ---------
        raise NotImplementedError("TODO: implement _is_plausible_plural_of_vocab")

    # ---------------- PROVIDED (keep) ----------------
    def _reasonable_df(self, df: int) -> bool:
        """
        Gate very rare tokens: require DF >= floor(max(2, 0.002 * total_docs)).
        This blocks weird names from stealing corrections.
        """
         # --------- TODO: implement reasonable dfs ---------
        raise NotImplementedError("TODO: implement _reasonable_df")

    # ---------------- STUDENT TODO ----------------
    def _levenshtein_distance(self, s1: str, s2: str) -> int:
        """
        Compute Levenshtein distance using a memory-light DP (row-based).

        Requirements:
        - Return non-negative integer distance.
        - Use O(min(n,m)) extra memory (no full matrix).
        """
        # --------- TODO: implement Levenshtein DP ---------
        raise NotImplementedError("TODO: implement _levenshtein_distance")

    # ---------------- STUDENT TODO ----------------
    def _correct_spelling(self, term: str) -> str:
        """
        Conservative correction using vocabulary + document frequency:

        Rules:
        - If term is very common/short (e.g., stop-like) or numeric → return as-is.
        - If term ∈ vocabulary → return as-is.
        - If term is a plausible plural of a vocab lemma → return as-is.
        - Else scan vocabulary candidates:
            * Length difference ≤ 2, same first letter (fast prefilter).
            * Levenshtein distance ≤ (1 if len(term) ≥ 5 else 2).
            * Candidate DF must pass _reasonable_df(df).
            * Prefer (distance, same_plural_class?, same_length?, -df) lexicographically.
        - If no suitable candidate → return original term.
        """
        # --------- TODO: implement conservative correction strategy ---------
        raise NotImplementedError("TODO: implement _correct_spelling")

    # ---------------- STUDENT TODO ----------------
    def parse(self, query: str) -> Tuple[Dict[str, float], str]:
        """
        Parse a user query to extract field weights and search terms.

        Steps:
        1) Start with a copy of self.default_weights.
        2) Preprocess quotes via _preprocess_quotes.
        3) Split into parts by whitespace.
        4) For each part:
           - If matches field:weight:
               • Map aliases (summary→summaries).
               • If field allowed: parse float; clamp negatives to 0.0; set.
               • Else: treat the whole part as a normal term.
           - Else:
               • Correct spelling via _correct_spelling and append to terms.
        5) Return (weights_dict, "terms string" preserving order).
        """
        # --------- TODO: implement query parsing workflow ---------
        raise NotImplementedError("TODO: implement parse")

    # ---------------- STUDENT TODO ----------------
    def _preprocess_quotes(self, query: str) -> str:
        """
        Remove single/double quotes but keep inner content to preserve term order.
        (Advanced handling of phrases is not required here.)
        """
        # --------- TODO: implement basic quote cleanup ---------
        raise NotImplementedError("TODO: implement _preprocess_quotes")

    # ---------------- STUDENT TODO ----------------
    def validate_weights(self, weights: Dict[str, float]) -> bool:
        """
        Validate field weights for correctness.

        Checks:
        - All keys belong to self.allowed_fields.
        - All values are numeric (int/float).
        - Warn (or normalize later) if any value is negative.

        Return:
            bool indicating whether weights are acceptable.
        """
        # --------- TODO: implement weight validation ---------
        raise NotImplementedError("TODO: implement validate_weights")

    # ---------------- PROVIDED (keep) ----------------
    def normalize_weights(self, weights: Dict[str, float]) -> Dict[str, float]:
        """Clamp negative weights to zero (simple normalization)."""
        normalized = weights.copy()
        for field in normalized:
            if normalized[field] < 0:
                logger.info(f"Normalizing negative weight for '{field}' to 0.0")
                normalized[field] = 0.0
        return normalized

    # ---------------- PROVIDED (keep) ----------------
    def parse_and_validate(self, query: str) -> Tuple[Dict[str, float], str]:
        """Parse a query and return normalized, validated weights along with terms."""
        weights, terms = self.parse(query)
        if not self.validate_weights(weights):
            logger.error("Query parsing produced invalid weights - using defaults")
            weights = self.default_weights.copy()
        return self.normalize_weights(weights), terms


def main() -> None:
    """
    Minimal driver for local testing.

    Usage:
        python query_parser.py "stars:1 title:0 summary:0.4 genres:0 drama prison"
    """
    try:
        parser = QueryParser()
        sample_query = "stars:1 title:0 summary:0.4 genres:0 spiedr man"
        weights, terms = parser.parse(sample_query)
        print("\nParsed query:", sample_query)
        print("Field weights:", weights)
        print("Search terms:", terms)
    except NotImplementedError as nie:
        logger.error(f"Unimplemented part of the assignment: {nie}")
        raise SystemExit(2)
    except Exception as e:
        logger.exception("Fatal error in query parser")
        print(f"\nQuery parser failed: {str(e)}")
        raise SystemExit(1)

if __name__ == "__main__":
    main()
