import io
from typing import List, TypedDict
import pdfplumber
import pandas as pd
import chardet
from docx import Document
import zipfile
import os
from bs4 import BeautifulSoup


def read_file_by_mime_type(file_content: bytes, mime_type: str) -> str:
    """
    Read file content and return as string based on mime type.

    Args:
        file_content: Raw file content as bytes
        mime_type: MIME type of the file

    Returns:
        File content as string
    """
    try:
        if mime_type in ["text/plain", "text/markdown"]:
            return _read_text_file(file_content)

        elif mime_type == "text/html":
            return _read_html_file(file_content)

        elif mime_type == "application/pdf":
            return _read_pdf_file(file_content)

        elif (
            mime_type
            == "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
        ):
            return _read_docx_file(file_content)

        elif mime_type == "application/msword":
            raise ValueError(
                "DOC files are not supported. "
                "Please convert your DOC file to DOCX format and try again."
            )

        elif mime_type in [
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            "application/vnd.ms-excel",
        ]:
            return _read_excel_file(file_content, mime_type)

        else:
            raise ValueError(f"Unsupported mime type: {mime_type}")

    except Exception as e:
        raise Exception(
            f"While reading file with mime type `{mime_type}`, "
            f"following error was encountered: {str(e)}"
        )


def _read_text_file(file_content: bytes) -> str:
    """Read text file with encoding detection."""

    # Detect encoding
    detected = chardet.detect(file_content)
    encoding = detected["encoding"] or "utf-8"

    # Handle BOM
    if file_content.startswith(b"\xef\xbb\xbf"):
        encoding = "utf-8-sig"
    elif file_content.startswith((b"\xff\xfe", b"\xfe\xff")):
        encoding = "utf-16"

    return file_content.decode(encoding)


def _read_html_file(file_content: bytes) -> str:
    """Read HTML file and extract text content."""
    # Detect encoding
    detected = chardet.detect(file_content)
    encoding = detected["encoding"] or "utf-8"

    # Handle BOM
    if file_content.startswith(b"\xef\xbb\xbf"):
        encoding = "utf-8-sig"
    elif file_content.startswith((b"\xff\xfe", b"\xfe\xff")):
        encoding = "utf-16"

    html_content = file_content.decode(encoding, errors="replace")

    # Parse HTML and extract text
    soup = BeautifulSoup(html_content, "html.parser")

    # Remove script and style elements
    for script in soup(["script", "style"]):
        script.decompose()

    # Get text and clean up whitespace
    text = soup.get_text(separator="\n")

    # Clean up excessive whitespace
    lines = (line.strip() for line in text.splitlines())
    chunks = (phrase.strip() for line in lines for phrase in line.split("  "))
    text = "\n".join(chunk for chunk in chunks if chunk)

    return text


def _read_pdf_file(file_content: bytes) -> str:
    """
    Read PDF file and extract text with page markers for lineage tracking.

    Inserts page markers in the format <<PAGE:N>> where N is the 1-indexed page number.
    These markers allow downstream chunking to preserve page number metadata.
    """
    with pdfplumber.open(io.BytesIO(file_content)) as pdf:
        text = ""
        for page_num, page in enumerate(pdf.pages, start=1):
            page_text = page.extract_text()
            if page_text:
                # Add page marker for lineage tracking
                text += f"<<PAGE:{page_num}>>\n{page_text}\n"
    return text


def _read_docx_file(file_content: bytes) -> str:
    """Read DOCX file and extract text."""
    doc = Document(io.BytesIO(file_content))
    text = ""

    # Extract paragraphs
    for paragraph in doc.paragraphs:
        text += paragraph.text + "\n"

    # Extract tables
    for table in doc.tables:
        for row in table.rows:
            for cell in row.cells:
                text += cell.text + " "
            text += "\n"

    return text


def _looks_like_binary_excel(first_bytes: bytes) -> bool:
    """Check if bytes look like actual Excel file format."""
    # Excel file signatures
    excel_signatures = [
        b"PK\x03\x04",  # ZIP-based format (.xlsx)
        b"\xd0\xcf\x11\xe0",  # OLE format (.xls)
        b"\x09\x08\x06\x00",  # Another .xls variant
        b"\x09\x08\x05\x00",  # Another .xls variant
    ]

    return any(first_bytes.startswith(sig) for sig in excel_signatures)


def _read_excel_file(file_content: bytes, mime_type: str) -> str:
    """Read Excel file or UTF-16 text file and convert to text representation."""

    # Check first few bytes to detect if it's actually a text file
    first_bytes = file_content[:100]

    # Check for UTF-16 or UTF-8 BOM markers
    is_text_file = (
        first_bytes.startswith(b"\xff\xfe")  # UTF-16 LE BOM
        or first_bytes.startswith(b"\xfe\xff")  # UTF-16 BE BOM
        or first_bytes.startswith(b"\xef\xbb\xbf")  # UTF-8 BOM
        or
        # More specific check: high density of null bytes suggests UTF-16 text
        (
            first_bytes.count(b"\x00") > len(first_bytes) * 0.3
            and not _looks_like_binary_excel(first_bytes)
        )
    )

    if is_text_file:
        return _read_text_masquerading_as_excel(file_content)
    else:
        return _read_actual_excel_file(file_content, mime_type)


def _read_text_masquerading_as_excel(file_content: bytes) -> str:
    """Handle UTF-16 encoded text files and format as markdown table."""
    import chardet

    # Detect and decode content
    detected = chardet.detect(file_content)
    encoding = detected["encoding"] or "utf-16"

    if file_content.startswith(b"\xff\xfe"):
        encoding = "utf-16-le"
    elif file_content.startswith(b"\xfe\xff"):
        encoding = "utf-16-be"
    elif file_content.startswith(b"\xef\xbb\xbf"):
        encoding = "utf-8-sig"
    elif b"\x00" in file_content[:50]:
        encoding = "utf-16"

    try:
        content = file_content.decode(encoding)
    except UnicodeDecodeError:
        content = file_content.decode("utf-16", errors="replace")

    lines = content.split("\n")

    # Find meaningful lines
    meaningful_lines = []
    for line in lines:
        stripped = line.rstrip()
        if stripped:  # Non-empty line
            meaningful_lines.append(stripped)

    if not meaningful_lines:
        return "No content found"

    # Start building markdown output
    result = "File content:\n\n"

    # First line is typically the title/header
    if meaningful_lines:
        title_line = meaningful_lines[0]
        result += f'"{title_line}"\n\n'

    # Look for tabular data (lines with multiple tab-separated values)
    table_lines = []
    for line in meaningful_lines[1:]:  # Skip the title line
        if "\t" in line:
            # Split on tabs and clean up each cell
            cells = [cell.strip() for cell in line.split("\t")]
            # Only include if it has multiple cells
            if len(cells) > 1:
                table_lines.append(cells)

    if table_lines:
        # First table line is the header
        if len(table_lines) > 0:
            headers = table_lines[0]
            data_rows = table_lines[1:]

            # Create markdown table
            # Header row
            result += "| " + " | ".join(headers) + " |\n"

            # Separator row
            result += "|" + "|".join([" --- " for _ in headers]) + "|\n"

            # Data rows
            for row in data_rows:
                # Pad row with empty strings if it's shorter than headers
                padded_row = row + [""] * (len(headers) - len(row))
                # Truncate if longer than headers
                padded_row = padded_row[: len(headers)]
                result += "| " + " | ".join(padded_row) + " |\n"

    return result


def _clean_dataframe(df: pd.DataFrame) -> pd.DataFrame:
    """Remove empty rows and columns from dataframe."""

    # Remove completely empty rows
    df = df.dropna(how="all")

    # Remove completely empty columns
    df = df.dropna(axis=1, how="all")

    # Remove columns that are all unnamed (like 'Unnamed: 1', 'Unnamed: 2')
    cols_to_drop = [col for col in df.columns if str(col).startswith("Unnamed:")]
    if cols_to_drop:
        # Check if these columns actually have data
        for col in cols_to_drop[:]:  # Copy list to avoid modification during iteration
            if not df[col].dropna().empty:
                cols_to_drop.remove(col)  # Keep columns with data

    df = df.drop(columns=cols_to_drop)

    # Reset index after removing rows
    df = df.reset_index(drop=True)

    return df


def _dataframe_to_markdown(df: pd.DataFrame) -> str:
    """Convert dataframe to markdown table format."""

    if df.empty:
        return "*No data*"

    # Handle column names
    columns = []
    for col in df.columns:
        # Clean up column names
        col_str = str(col) if pd.notna(col) else ""
        columns.append(col_str)

    # Build markdown table
    markdown = ""

    # Header row
    markdown += "| " + " | ".join(columns) + " |\n"

    # Separator row
    markdown += "|" + "|".join([" --- " for _ in columns]) + "|\n"

    # Data rows
    for _, row in df.iterrows():
        cells = []
        for value in row:
            # Convert cell value to string, handling NaN and None
            if pd.isna(value):
                cell_str = ""
            else:
                cell_str = str(value).strip()
            cells.append(cell_str)

        markdown += "| " + " | ".join(cells) + " |\n"

    return markdown


def _detect_file_type(file_data: bytes) -> str:
    """Detect file type from bytes."""

    if file_data.startswith(b"%PDF"):
        return "PDF"
    elif file_data.startswith(b"PK\x03\x04"):
        return "ZIP/Office"
    elif file_data.startswith(b"\xff\xd8\xff"):
        return "JPEG"
    elif file_data.startswith(b"\x89PNG"):
        return "PNG"
    elif file_data.startswith(b"\xd0\xcf\x11\xe0"):
        return "OLE/Legacy Office"
    else:
        return "Unknown"


class EmbeddedFileInfo(TypedDict):
    file_path: str
    file_content: bytes
    file_type: str
    size: int


def extract_embedded_files_from_xlsx(
    file_content: bytes, output_dir: str = "extracted_files"
) -> List[EmbeddedFileInfo]:
    """Extract embedded files from .xlsx file."""

    # Create output directory
    os.makedirs(output_dir, exist_ok=True)

    embedded_files: List[EmbeddedFileInfo] = []

    with zipfile.ZipFile(io.BytesIO(file_content), "r") as zip_ref:
        # List all files in the ZIP
        file_list = zip_ref.namelist()

        # Look for embedded objects in common locations
        embedded_paths = [
            "xl/embeddings/",  # Embedded files folder
            "xl/media/",  # Images and media
            "xl/drawings/",  # Drawing objects
            "word/embeddings/",  # Alternative location
        ]

        for file_path in file_list:
            # Check if file is in embedded locations
            is_embedded = any(file_path.startswith(path) for path in embedded_paths)

            # Also check for specific embedded file patterns
            is_embedded = is_embedded or (
                "/oleObject" in file_path
                or file_path.endswith(
                    (".pdf", ".doc", ".docx", ".ppt", ".pptx", ".txt")
                )
                or "bin" in file_path.lower()
            )

            if is_embedded:
                try:
                    # Extract the file
                    file_data = zip_ref.read(file_path)

                    embedded_files.append(
                        EmbeddedFileInfo(
                            file_path=file_path,
                            size=len(file_data),
                            file_type=_detect_file_type(file_data),
                            file_content=file_data,
                        )
                    )

                except Exception as e:
                    print(f"Error extracting {file_path}: {e}")

    return embedded_files


def _is_excel_file(file_path: str, file_type: str) -> bool:
    """Check if extracted file is an Excel file."""

    # Check by file type detection
    if file_type in ["ZIP/Office"]:
        return True

    # Check by file extension
    path_lower = file_path.lower()
    if path_lower.endswith((".xlsx", ".xls")):
        return True

    # Check by reading first few bytes of the actual file
    try:
        with open(file_path, "rb") as f:
            first_bytes = f.read(100)

        # Excel file signatures
        excel_signatures = [
            b"PK\x03\x04",  # ZIP-based format (.xlsx)
            b"\xd0\xcf\x11\xe0",  # OLE format (.xls)
        ]

        return any(first_bytes.startswith(sig) for sig in excel_signatures)
    except Exception:
        return False


def _get_mime_type_from_path(file_path: str) -> str:
    """Get mime type from file path."""

    path_lower = file_path.lower()

    if path_lower.endswith(".xlsx"):
        return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    elif path_lower.endswith(".xls"):
        return "application/vnd.ms-excel"
    else:
        # Default to xlsx if uncertain
        return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"


def _read_actual_excel_file(file_content: bytes, mime_type: str, depth: int = 0) -> str:
    """Read actual binary Excel files and format as clean markdown."""

    # Limit recursion depth to prevent infinite loops
    if depth > 3:
        return "Maximum embedding depth reached"

    if mime_type == "application/vnd.ms-excel":  # .xls files
        try:
            all_sheets = pd.read_excel(
                io.BytesIO(file_content), sheet_name=None, engine="xlrd"
            )
        except ImportError:
            raise ImportError(
                "xlrd required for .xls files. Install with: pip install xlrd==1.2.0"
            )
        except Exception as e:
            raise Exception(f"Error reading .xls file: {str(e)}")

    else:  # .xlsx files
        try:
            all_sheets = pd.read_excel(
                io.BytesIO(file_content), sheet_name=None, engine="openpyxl"
            )
        except ImportError:
            raise ImportError(
                "openpyxl required for .xlsx files. Install with: pip install openpyxl"
            )
        except Exception as e:
            raise Exception(f"Error reading .xlsx file: {str(e)}")

    # Convert to markdown
    text = ""
    for sheet_name, df in all_sheets.items():
        # Clean the dataframe
        cleaned_df = _clean_dataframe(df)

        if not cleaned_df.empty:
            text += f"## Sheet: {sheet_name}\n\n"
            text += _dataframe_to_markdown(cleaned_df)
            text += "\n\n"
        # else:
        #     text += f"## Sheet: {sheet_name}\n\n*No data found*\n\n"

    # Check for embedded files
    try:
        embedded_files = extract_embedded_files_from_xlsx(file_content)
        if len(embedded_files) > 0:
            filtered = list(
                filter(
                    lambda x: _is_excel_file(x["file_path"], x["file_type"]),
                    embedded_files,
                )
            )

            if len(filtered) > 0:
                text += "## Embedded Files\n\n"

            # If embedded file is an Excel file, read it recursively
            for file_info in filtered:
                try:
                    # Determine mime type for embedded file
                    embedded_mime = _get_mime_type_from_path(file_info["file_path"])

                    text += f"\n### Content of {file_info['file_path']}:\n\n"
                    nested_content = _read_actual_excel_file(
                        file_info["file_content"], embedded_mime, depth + 1
                    )
                    text += nested_content
                    text += "\n"

                except Exception as e:
                    text += f"\n*Error reading embedded Excel file: {e}*\n"

            text += "\n"
    except Exception as e:
        text += f"## Embedded Files\n\nError extracting: {e}\n\n"

    return text
