import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

# --- EXERCISE 1: La disparition (No 'e' or 'E') ---
class LaDisparition:
    """
    Generate text without ever using the letter 'e' or 'E'.
    
    You must use model() directly: model(input_ids) yields logits.
    You need to manually adjust the logits to forbid tokens containing 'e' or 'E'.
    
    REQUIREMENT: Do NOT use model.generate().
    
    Hints:
    - In __init__, pre-compute the set of forbidden token IDs by checking
      which tokens in the vocabulary decode to strings containing 'e' or 'E'.
    - In __call__, implement a token-by-token generation loop:
      1. Feed the current sequence to the model to get logits
      2. Mask out forbidden tokens (set their logits to -inf)
      3. Pick the next token (greedy: argmax, or use beam search for better quality)
      4. Append and repeat
    - Return only the generated text (not the prompt).
    """
    def __init__(self, model: AutoModelForCausalLM, tokenizer: AutoTokenizer):
        self.model = model
        self.tokenizer = tokenizer
        
        # TODO: Pre-calculate forbidden token IDs
        # Hint: also consider forbidding non-ASCII tokens that might hide the letter 'e'.
        # YOUR CODE HERE

    def __call__(self, prompt, max_tokens=20):
        # Tokenize the prompt using the chat template
        message = [{"role": "user", "content": prompt}]
        encoded = self.tokenizer.apply_chat_template(
            message, add_generation_prompt=True, return_tensors="pt"
        )
        input_ids = (encoded if isinstance(encoded, torch.Tensor) else encoded["input_ids"]).to(self.model.device)
        prompt_len = input_ids.shape[1]

        # TODO: Implement constrained generation loop
        # return only the generated text (after the prompt).
        
        # YOUR CODE HERE
        
        raise NotImplementedError("Implement constrained generation without 'e'")


# --- EXERCISE 2: The Toulouse Sequence ---
class ToulouseSequence:
    """
    Generate text without ever producing the word 'Toulouse'.
    
    You must use model() directly: model(input_ids) yields logits.
    
    REQUIREMENT: Do NOT use model.generate().
    
    This is harder than Exercise 1 because 'Toulouse' spans multiple tokens.
    """
    def __init__(self, model: AutoModelForCausalLM, tokenizer: AutoTokenizer):
        self.model = model
        self.tokenizer = tokenizer
        self.forbidden_word = "Toulouse"

    def __call__(self, prompt, max_tokens=20):
        # Tokenize the prompt using the chat template
        message = [{"role": "user", "content": prompt}]
        encoded = self.tokenizer.apply_chat_template(
            message, add_generation_prompt=True, return_tensors="pt"
        )
        inputs = (encoded if isinstance(encoded, torch.Tensor) else encoded["input_ids"]).to(self.model.device)
        prompt_length = inputs.shape[1]

        # TODO: Implement constrained generation loop
        # Return only the generated text (after the prompt).        
        # YOUR CODE HERE
        
        raise NotImplementedError("Implement constrained generation without 'Toulouse'")


if __name__ == "__main__":
    # NOTE: This block is for local testing only.
    # The evaluation server provides model and tokenizer.
    # You can use any small model for testing, e.g.:
    MODEL_NAME = "HuggingFaceTB/SmolLM2-1.7B-Instruct"
    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
    model = AutoModelForCausalLM.from_pretrained(
        MODEL_NAME, dtype=torch.float16, device_map="auto"
    )

    print("=== Exercise 1: La Disparition (no 'e') ===")
    ex1 = LaDisparition(model, tokenizer)
    result = ex1("Who is the king of the jungle?")
    print(f"Result: {result}")
    has_e = 'e' in result.lower()
    print(f"Contains 'e': {has_e} {'✗ FAIL' if has_e else '✓ PASS'}")

    print("\n=== Exercise 2: No Toulouse ===")
    ex2 = ToulouseSequence(model, tokenizer)
    result = ex2("Where are the headquarters of Airbus located?")
    print(f"Result: {result}")
    has_toulouse = 'toulouse' in result.lower()
    print(f"Contains 'Toulouse': {has_toulouse} {'✗ FAIL' if has_toulouse else '✓ PASS'}")
