import json
import os
import random
import string
import re
import base64
from fastapi_mail import FastMail, MessageSchema, ConnectionConfig
from fastapi import HTTPException, status
from fastapi.responses import JSONResponse
import xml.etree.ElementTree as ET
import xml.dom.minidom as minidom
import uuid
import logging
from datetime import date

CONTAINER_RE = re.compile(r'^(Module|Lesson|Unit)\s+\d+\b', re.IGNORECASE)

def generate_random_text():
    return ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))

def replace_special_characters(input_string):
    # Replace all non-alphanumeric characters (including spaces) with an underscore
    sanitized_string = re.sub(r'[^a-zA-Z0-9]', '_', input_string)
    return sanitized_string

def sanitize_filename(name):
    """Remove invalid characters for filenames."""
    return re.sub(r'[\\/*?:"<>|]', '_', name).strip()

def get_error_message(response):
    messages = [error["message"] for error in response.get("errors", [])]
    return messages

async def send_email(recipient: str, subject: str, body: str):

    conf = ConnectionConfig(
        MAIL_USERNAME=os.getenv('SES_ACCESS_KEY_ID'),
        MAIL_PASSWORD=os.getenv('SES_SECRET_ACCESS_KEY'),
        MAIL_FROM=os.getenv('SES_SENDER_EMAIL'),
        MAIL_PORT=465,
        MAIL_SERVER=os.getenv('SES_AWS_HOST'),
        MAIL_STARTTLS=False,
        MAIL_SSL_TLS=True,
        USE_CREDENTIALS=True
    )
    
    message = MessageSchema(
        subject=subject,
        recipients=[recipient],  # List of recipients
        body=body,
        subtype="html"
    )
    fm = FastMail(conf)
    try:
        await fm.send_message(message)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

def read_json_file(file_path):
    """
    Reads a JSON file and returns its content as a Python dictionary or list,
    depending on the JSON structure.

    Args:
        file_path (str): The path to the JSON file to be read.

    Returns:
        dict or list: The content of the JSON file. The type depends on the
                    structure of the JSON file (object or array at the root).
    """
    with open(file_path) as f:
        return json.load(f)

def read_txt_file(file_path):
    with open(file_path) as f:
        return f.read()


def parse(text):
    regex = re.compile(r']:\s+"(.*?)"\s+http')
    text = regex.sub("]: http", text)
    return text

def construct_citation_dict_from_article(search_results, index_to_find):
    """Constructs a citation dictionary from search results.
    Args:
        search_results (dict): Dictionary containing search results.
        index_to_find (int): Index of the citation to find.
    Returns:
        dict: A dictionary containing the citation information for the specified index.
    """
    if search_results is None:
        return None
    citation_dict = {}
    for url, index in search_results["url_to_unified_index"].items():
        citation_dict[index] = {
            "url": url,
            "title": search_results["url_to_info"][url]["title"],
            "snippets": search_results["url_to_info"][url]["snippets"],
        }
    return citation_dict.get(index_to_find, None)

def create_manifest(course_title: str, content_dir: str, wiki_dir: str, output_file: str = "imsmanifest.xml"):
    """Creates an IMSCC manifest XML file for a course.
    Args:
        course_title (str): Title of the course.
        content_dir (str): Directory containing course content.
        wiki_dir (str): Directory for wiki content.
        output_file (str): Path to save the generated manifest file.
    Returns:
        None: The function writes the manifest XML to the specified output file.
    """
    ET.register_namespace('', "http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1")
    ET.register_namespace('imsqti', "http://www.imsglobal.org/xsd/ims_qtiasiv1p2")
    ET.register_namespace('lom', "http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource")
    ET.register_namespace('lomimscc', "http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest")
    ET.register_namespace('xsi', "http://www.w3.org/2001/XMLSchema-instance")

    manifest = ET.Element("manifest", {
        "identifier": f"man_{uuid.uuid4().hex}",
        "xmlns": "http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1",
        "xmlns:imsqti": "http://www.imsglobal.org/xsd/ims_qtiasiv1p2",
        "xmlns:lom": "http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource",
        "xmlns:lomimscc": "http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest",
        "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
        "xsi:schemaLocation": (
            "http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1 "
            "http://www.imsglobal.org/profile/cc/ccv1p1/ccv1p1_imscp_v1p2_v1p0.xsd "
            "http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource "
            "http://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lomresource_v1p0.xsd "
            "http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest "
            "http://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lommanifest_v1p0.xsd"
        )
    })

    # Canvas extension namespace
    CANVAS_NS = "http://canvas.instructure.com/x-manifest"

    metadata = ET.SubElement(manifest, "metadata")
    ET.SubElement(metadata, "schema").text = "IMS Common Cartridge"
    ET.SubElement(metadata, "schemaversion").text = "1.1.0"

    lom = ET.SubElement(metadata, "lomimscc:lom")
    general = ET.SubElement(lom, "lomimscc:general")
    title = ET.SubElement(general, "lomimscc:title")
    ET.SubElement(title, "lomimscc:string").text = course_title

    lifeCycle = ET.SubElement(lom, "lomimscc:lifeCycle")
    contribute = ET.SubElement(lifeCycle, "lomimscc:contribute")
    lifeCycleDate = ET.SubElement(contribute,"lomimscc:date")
    today = date.today()
    formatted_date = today.strftime("%Y-%m-%d")
    
    ET.SubElement(lifeCycleDate, "lomimscc:dateTime").text = f"{formatted_date}"

    lomRights = ET.SubElement(lom,"lomimscc:rights")
    copyrights = ET.SubElement(lomRights,"lomimscc:copyrightAndOtherRestrictions")
    ET.SubElement(copyrights,"lomimscc:value").text = "yes"

    copyrightDescription = ET.SubElement(lomRights,"lomimscc:description")
    ET.SubElement(copyrightDescription,"lomimscc:string").text = "Private (Copyrighted) - http://en.wikipedia.org/wiki/Copyright"


    # <organizations>
    organizations = ET.SubElement(manifest, "organizations")
    org = ET.SubElement(organizations, "organization", {"identifier": "org_1", "structure": "rooted-hierarchy"})

    resources_el = ET.SubElement(manifest, "resources")

    i = 0
    item_counter = 0
    module_counter = 0
    modules = {}  # track module folder → XML <item>
    
    for dirpath, _, filenames in os.walk(content_dir):

        folder_name = os.path.basename(dirpath)
        is_module_folder = re.match(r"^(module|content)\s*\d+", folder_name, re.IGNORECASE)

        if is_module_folder and folder_name not in modules:
            mod_id = f"module_{len(modules)+1}"
            modules[folder_name] = ET.SubElement(org, "item", {"identifier": mod_id})
            ET.SubElement(modules[folder_name], "title").text = folder_name.title()


        for filename in sorted(filenames):
            item_counter += 1
            item_id = f"item_{item_counter}"
            res_id = f"res_{item_counter}"
            rel_path = os.path.relpath(os.path.join(dirpath, filename), content_dir).replace("\\", "/")
            ext = os.path.splitext(filename)[1].lower()
            # base_title = os.path.splitext(filename)[0].replace("_", " ").replace("-", " ").title()
            base_title = filename.replace("_", " ").replace(".html", "").replace(".xml", "").title().replace("-", " ")

            
            # Determine resource type based on file extension
            if re.match(r"(course[-_ ]?(description|overview|outcomes))", filename, re.IGNORECASE):
                intendeduse = "syllabus"
                rtype = "webcontent"
                display_title = base_title
            elif ext == ".html":
                intendeduse = "lesson"
                rtype = "associatedcontent/imscc_xmlv1p1/learning-application-resource"
                # rtype = "webcontent"
                display_title = f"Lesson: {base_title}"
            elif ext in [".pdf", ".docx", ".pptx"]:
                intendeduse = None
                rtype = "associatedcontent/imscc_xmlv1p1/learning-application-resource"
                display_title = f"File: {base_title}"
            elif ext in [".xml", ".qti"]:
                intendeduse = None
                rtype = "imsqti_xmlv1p2/imscc_xmlv1p1/assessment"
                display_title = f"Quiz: {base_title}"
            else:
                intendeduse = "webcontent"
                rtype = None
                display_title = base_title
            
            parent_item = modules[folder_name] if is_module_folder else org

            nested_item = ET.SubElement(parent_item, "item", {"identifier": item_id, "identifierref": res_id})
            ET.SubElement(nested_item, "title").text = display_title

            # Canvas module_item extension
            ext_el = ET.SubElement(nested_item, "extension", {"xmlns": CANVAS_NS})
            ET.SubElement(ext_el, "module_item")

            # Relative path for manifest
            res_attrs = {"identifier": res_id, "type": rtype, "href": rel_path}
            if intendeduse:  # only add if not None
                res_attrs["intendeduse"] = intendeduse
            resource = ET.SubElement(resources_el, "resource", res_attrs)
            ET.SubElement(resource, "file", {"href": rel_path})

    tree = ET.ElementTree(manifest)
    ET.indent(tree, space="  ", level=0)
    tree.write(output_file, encoding="utf-8", xml_declaration=True)

    print(f"✅ imsmanifest.xml created at: {output_file}")

# Create QTI XML for a module with multiple-choice questions
# Example questions format:
# questions = [
#     {
#         "question": "What is the capital of France?",
#         "options": ["Paris", "London", "Berlin", "Madrid"],
#         "answer": "A"  # Correct answer option (A, B, C, D)
#     },
#     {
#         "question": "What is 2 + 2?",
#         "options": ["3", "4", "5", "6"],
#         "answer": "B"  # Correct answer option (A, B, C, D)
#     }
# ]
#

def create_qti_for_module(module_name: str, questions: list, output_path: str):
    """Creates a QTI XML file for a module with multiple-choice questions.
    Args:
        module_name (str): Name of the module.
        questions (list): List of dictionaries containing question data.
        output_path (str): Path to save the generated QTI XML file.
    Returns:
        None: The function writes the QTI XML to the specified output path.
    """
    qtins = "http://www.imsglobal.org/xsd/ims_qtiasiv1p2"

    questestinterop = ET.Element("questestinterop", {"xmlns": qtins})
    assessment = ET.SubElement(questestinterop, "assessment", {"ident": str(uuid.uuid4()), "title": module_name})
    section = ET.SubElement(assessment, "section", {"ident": "root_section"})

    for i, q in enumerate(questions, start=1):
        item = ET.SubElement(section, "item", {"ident": str(uuid.uuid4()), "title": f"Question {i}"})
        presentation = ET.SubElement(item, "presentation")

        mattext = ET.SubElement(ET.SubElement(presentation, "material"), "mattext")
        mattext.text = q["question"]

        response_lid = ET.SubElement(
            presentation, "response_lid", {"ident": f"Q{i}", "rcardinality": "Single"}
        )
        render_choice = ET.SubElement(response_lid, "render_choice")

        for j, opt in enumerate(q["options"]):
            ident = chr(65 + j)
            resp = ET.SubElement(render_choice, "response_label", {"ident": ident})
            mat = ET.SubElement(resp, "material")
            mattext = ET.SubElement(mat, "mattext")
            mattext.text = opt

        resprocessing = ET.SubElement(item, "resprocessing")
        outcomes = ET.SubElement(resprocessing, "outcomes")
        ET.SubElement(outcomes, "decvar", {"maxvalue": "100", "minvalue": "0", "varname": "SCORE"})

        respcondition = ET.SubElement(resprocessing, "respcondition", {"continue": "No"})
        conditionvar = ET.SubElement(respcondition, "conditionvar")
        ET.SubElement(conditionvar, "varequal", {"respident": f"Q{i}"}).text = q["answer"]

        ET.SubElement(respcondition, "setvar", {"action": "Set"}).text = "100"

    tree = ET.ElementTree(questestinterop)
    ET.indent(tree, space="  ", level=0)
    module_name = replace_special_characters(module_name)
    # os.makedirs(output_path, exist_ok=True)
    tree.write(os.path.join(output_path, f"{module_name}_quiz.xml"), encoding="utf-8", xml_declaration=True)

def generate_resources_from_folder(folder_path: str, base_path: str = "wiki_content") -> list:
    """Generates a list of resources from a folder containing HTML files.
    Args:
        folder_path (str): Path to the folder containing HTML files.
        base_path (str): Base path to prepend to the resource hrefs.
    Returns:
        list: A list of dictionaries representing the resources.
    """
    resources = []
    for i, filename in enumerate(os.listdir(folder_path), start=1):
        if not filename.lower().endswith(".html"):
            continue  # skip non-html files

        full_path = os.path.join(base_path, filename)
        resources.append({
            "id": f"res_{i}",
            "type": "webcontent",
            "href": full_path
        })
    return resources

def normalize_spaces(input_string):
    return re.sub(r'\s+', ' ', input_string).strip()

def split_markdown_sections(md_text: str) -> list:
    """Splits the markdown into sections based on top-level (#) headings."""
    # sections = re.split(r'(?:^|\n)(?=##?\s)(?=^### Module \d+:)', md_text, flags=re.MULTILINE)
    # return [s.strip() for s in sections if s.strip()]
    parts = re.split(r'(?=^(##\s.*|### Module \d+:))', md_text, flags=re.MULTILINE)
    
    sections = []
    for i in range(0, len(parts), 2):
        section = parts[i]
        if i + 1 < len(parts):
            section += parts[i + 1]
        sections.append(section.strip())
    return sections

def bold_markdown_keywords(md_content: str) -> str:
    # Keywords to bold (case-insensitive)
    keywords = [r'Question \d+:', 'Correct Answer:', 'Subtopics:', 'Description:']

    for keyword in keywords:
        # Match the keyword only at the beginning of a line
        pattern = rf"^(?P<key>{keyword})"
        md_content = re.sub(pattern, r'**\g<key>**', md_content, flags=re.MULTILINE)

    return md_content

def truncate_filename(filename, max_length=125):
    """Truncate filename to max_length to ensure the filename won't exceed the file system limit.

    Args:
        filename: str
        max_length: int, default to 125 (usual path length limit is 255 chars)
    """

    if len(filename) > max_length:
        truncated_filename = filename[:max_length]
        logging.warning(
            f"Filename is too long. Filename is truncated to {truncated_filename}."
        )
        return truncated_filename

    return filename

def get_prompt(prompt_name, prompt_type=None):
    """
    Retrieves a prompt from the prompts directory based on the prompt name and topic.
    
    Args:
        prompt_name (str): The name of the prompt key.
        prompt_type (str): The type of the prompt, e.g., 'course', 'module', etc.
        
    Returns:
        str: The content of the prompt file.
    """
    
    prompts_dir = os.getenv('PROMPTS_DIR', 'prompts')
    prompt_file = os.path.join(prompts_dir, f"prompt_data.json")
    
    prompt_content = read_json_file(prompt_file)
    prompt_value = next((item["prompt"] for item in prompt_content if item["name"] == prompt_name and item['type'] ==  prompt_type), None)
    if prompt_value:
        return prompt_value
    else:
        return False
    
def slugify(text):
    return text.lower().replace(" ", "-").replace(":", "").replace(",", "")

def make_slug(text):
    return text.lower().strip()\
        .replace(' ', '-')\
        .replace(':', '')\
        .replace(',', '')\
        .replace('.', '')\
        .replace('/', '-')\
        .replace('?', '')\
        .replace('!', '')\
        .replace('\'', '')\
        .replace('"', '')

def is_container_heading(title: str) -> bool:
    return CONTAINER_RE.match(title) is not None

def build_nested_toc(soup):
    stack = []
    root = []
    current = None
    container_at_level = {} 

    for tag in soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'ul', 'ol']):
        if tag.name.startswith('h'):
            level = int(tag.name[1])
            title = tag.get_text().strip()
            is_container = is_container_heading(title)

            # Build full nested slug path
            # parent_slugs = [item['slug'] for item in stack]
            # current_slug = make_slug(title)
            # full_slug = '/'.join(parent_slugs + [current_slug])

            # Heuristic: if we are inside a container at this heading level
            # and the current heading is NOT a container but has the same level,
            # treat it as a child by bumping its effective level by +1
            effective_level = level
            if not is_container and container_at_level.get(level):
                effective_level = level + 1
            
            # Find parent for nesting
            while stack and stack[-1]['level'] >= effective_level:
                stack.pop()

            parent_path = stack[-1]['path'] if stack else ""
            current_slug = make_slug(title)
            full_path = f"{parent_path}/{current_slug}" if parent_path else current_slug

            node = {
                "level": effective_level,
                "title": title,
                "slug": full_path,
                "path": full_path,  # store the full path here
                "content": "",
                "children": []
            }

            if stack:
                stack[-1]['children'].append(node)
            else:
                root.append(node)

            stack.append(node)
            current = node

            # Remember the most recent container at this original level
            if is_container:
                container_at_level[level] = node
                # optional: clear deeper container memories when a new container opens
                for k in list(container_at_level.keys()):
                    if k > level:
                        del container_at_level[k]
        elif current:
            # current['content'] += tag.get_text().strip() + '\n\n'
            if tag.name in ['p']:
                if tag.find_parent(['ul', 'ol', 'li']) is None:
                    current['content'] += bold_markdown_keywords(tag.get_text().strip()) + '\n\n'
            elif tag.name in ['ul', 'ol']:
                items = tag.find_all('li', recursive=False)
                for item in items:
                    current['content'] += f"- {item.get_text().strip()}\n"
                current['content'] += '\n'
    # Remove 'path' key from output for clean JSON
    def strip_path_key(node):
        node.pop('path', None)
        # for child in node['children']:
        #     strip_path_key(child)
        # return node
        node['children'] = [strip_path_key(child) for child in node['children']]
        return node
    return [strip_path_key(n) for n in root]

def normalize_headings(soup):
    for tag in soup.find_all("p"):
        # if <p> has only one <strong> and no extra text
        if tag.strong and tag.get_text(strip=True) == tag.strong.get_text(strip=True):
            # Convert it into a heading (default h4)
            new_tag = soup.new_tag("h4")
            new_tag.string = tag.strong.get_text(strip=True)
            tag.replace_with(new_tag)
    return soup

def errorResponse(message,status_code = 400):
    if not status_code:
        status_code = status.HTTP_400_BAD_REQUEST

    return JSONResponse(
        status_code=status_code,
        content={
            "status": status_code,
            "message": message,
        },
    )
def successResponse(message, data):
	return JSONResponse(
        status_code=status.HTTP_200_OK,
        content={
            "status": status.HTTP_200_OK,
            "message": message,
            "data": data
        },
    )