from __future__ import annotations

import re
import shutil
import subprocess
from pathlib import Path
from xml.etree import ElementTree
from zipfile import BadZipFile, ZipFile

from app.services.knowledge_constants import IMAGE_EXTENSIONS, TEXT_EXTENSIONS
from app.services.knowledge_file_utils import extract_extension

MAX_EXTRACTED_XLSX_SHEETS = 12
MAX_EXTRACTED_XLSX_ROWS_PER_SHEET = 300
MAX_EXTRACTED_XLSX_COLUMNS = 40
MAX_EXTRACTED_PPTX_SLIDES = 80


def _read_text_preview(file_path: Path) -> str:
    encodings = ("utf-8", "utf-8-sig", "gbk")
    for encoding in encodings:
        try:
            return file_path.read_text(encoding=encoding)
        except UnicodeDecodeError:
            continue
    return "当前文本文件编码暂不支持在线解析。"


def _extract_docx_text(file_path: Path) -> str:
    try:
        with ZipFile(file_path) as archive:
            xml_content = archive.read("word/document.xml")
    except (BadZipFile, KeyError):
        return "当前 Word 文件解析失败。"

    root = ElementTree.fromstring(xml_content)
    body = next((node for node in root.iter() if node.tag.endswith("}body")), root)
    blocks: list[str] = []

    for child in body:
        if child.tag.endswith("}p"):
            paragraph = _extract_docx_paragraph_text(child)
            if paragraph:
                blocks.append(paragraph)
            continue
        if child.tag.endswith("}tbl"):
            table = _extract_docx_table_rows(child)
            rendered = _build_docx_table_markdown(table)
            if rendered:
                blocks.append(rendered)

    if blocks:
        return "\n\n".join(blocks)

    texts = [node.text.strip() for node in root.iter() if node.tag.endswith("}t") and node.text]
    return "\n".join(texts)


def _extract_docx_paragraph_text(node: ElementTree.Element) -> str:
    parts: list[str] = []
    for child in node.iter():
        if child.tag.endswith("}t") and child.text:
            parts.append(child.text)
        elif child.tag.endswith("}tab"):
            parts.append("\t")
        elif child.tag.endswith("}br"):
            parts.append("\n")
    return _normalize_docx_cell_text("".join(parts))


def _extract_docx_table_rows(table_node: ElementTree.Element) -> list[list[str]]:
    rows: list[list[str]] = []
    for row_node in table_node:
        if not row_node.tag.endswith("}tr"):
            continue

        row: list[str] = []
        for cell_node in row_node:
            if not cell_node.tag.endswith("}tc"):
                continue

            cell_parts = [
                _extract_docx_paragraph_text(paragraph)
                for paragraph in cell_node
                if paragraph.tag.endswith("}p")
            ]
            row.append(_normalize_docx_cell_text(" ".join(part for part in cell_parts if part)))

        if any(row):
            rows.append(row)
    return rows


def _build_docx_table_markdown(rows: list[list[str]]) -> str:
    visible_rows = [
        [_escape_markdown_cell(cell) for cell in row]
        for row in rows
        if any(str(cell or "").strip() for cell in row)
    ]
    if len(visible_rows) < 2:
        return ""

    column_count = max(len(row) for row in visible_rows)
    normalized_rows = [row + [""] * (column_count - len(row)) for row in visible_rows]
    header = [
        cell or f"列{column_index + 1}" for column_index, cell in enumerate(normalized_rows[0])
    ]
    body_rows = normalized_rows[1:]
    parts = [_format_markdown_table(header, body_rows)]

    row_clues: list[str] = []
    for row_number, row in enumerate(body_rows, start=2):
        pairs = [
            f"{header[column_index]}={value}"
            for column_index, value in enumerate(row)
            if value
        ]
        if pairs:
            row_clues.append(f"- 表格第 {row_number} 行：" + "；".join(pairs))

    if row_clues:
        parts.append("### 表格行级检索线索")
        parts.extend(row_clues)

    return "\n\n".join(parts)


def _normalize_docx_cell_text(value: str) -> str:
    normalized = str(value or "").replace("\r\n", "\n").replace("\r", "\n")
    normalized = re.sub(r"[ \t]*\n[ \t]*", " ", normalized)
    normalized = re.sub(r"\s+", " ", normalized)
    return normalized.strip()


def _extract_document_text_from_path(
    *,
    file_path: Path,
    original_name: str,
    mime_type: str,
) -> str:
    extension = extract_extension(original_name)
    if extension in TEXT_EXTENSIONS:
        return _normalize_extracted_text(_read_text_preview(file_path))
    if extension == "docx":
        return _normalize_extracted_text(_extract_docx_text(file_path))
    if extension == "xlsx":
        return _normalize_extracted_text(
            _build_xlsx_markdown(
                original_name=original_name,
                sheets=_extract_xlsx_sheets(file_path),
            )
        )
    if extension == "pptx":
        return _normalize_extracted_text(
            _build_pptx_markdown(
                original_name=original_name,
                slides=_extract_pptx_slides(file_path),
            )
        )
    if extension == "pdf":
        text = _normalize_extracted_text(_extract_pdf_text(file_path))
        if text:
            return text
        return _normalize_extracted_text(
            _extract_text_with_ocr(
                file_path=file_path,
                original_name=original_name,
                mime_type=mime_type,
            )
        )
    if extension in IMAGE_EXTENSIONS:
        return _normalize_extracted_text(
            _extract_text_with_ocr(
                file_path=file_path,
                original_name=original_name,
                mime_type=mime_type,
            )
        )
    return ""


def _normalize_extracted_text(text: str) -> str:
    normalized = str(text or "").replace("\r\n", "\n").replace("\r", "\n")
    normalized = re.sub(r"\n{3,}", "\n\n", normalized)
    return normalized.strip()


def _extract_pdf_text(file_path: Path) -> str:
    pdftotext_bin = shutil.which("pdftotext")
    if not pdftotext_bin:
        return ""

    completed = subprocess.run(
        [pdftotext_bin, "-layout", str(file_path), "-"],
        capture_output=True,
        text=True,
        timeout=40,
        check=False,
    )
    if completed.returncode != 0:
        return ""
    return str(completed.stdout or "")


def _extract_text_with_ocr(
    *,
    file_path: Path,
    original_name: str,
    mime_type: str,
) -> str:
    try:
        from app.services.ocr import OcrService

        result = OcrService().recognize_files([(original_name, file_path.read_bytes(), mime_type)])
    except Exception:
        return ""

    parts: list[str] = []
    for document in result.documents:
        text = str(getattr(document, "text", "") or "").strip()
        summary = str(getattr(document, "summary", "") or "").strip()
        if text:
            parts.append(text)
        elif summary:
            parts.append(summary)
    return "\n\n".join(part for part in parts if part)


def _extract_xlsx_sheets(file_path: Path) -> list[tuple[str, list[list[str]]]]:
    try:
        with ZipFile(file_path) as archive:
            shared_strings: list[str] = []
            if "xl/sharedStrings.xml" in archive.namelist():
                shared_root = ElementTree.fromstring(archive.read("xl/sharedStrings.xml"))
                shared_strings = [
                    "".join(node.itertext()).strip()
                    for node in shared_root.iter()
                    if node.tag.endswith("}si")
                ]

            sheet_files = sorted(
                name
                for name in archive.namelist()
                if re.fullmatch(r"xl/worksheets/sheet\d+\.xml", name)
            )
            if not sheet_files:
                return []

            relationship_targets: dict[str, str] = {}
            if "xl/_rels/workbook.xml.rels" in archive.namelist():
                rel_root = ElementTree.fromstring(archive.read("xl/_rels/workbook.xml.rels"))
                for node in rel_root.iter():
                    if not node.tag.endswith("Relationship"):
                        continue
                    rel_id = node.attrib.get("Id")
                    target = node.attrib.get("Target")
                    if not rel_id or not target:
                        continue
                    normalized = target.lstrip("/")
                    if not normalized.startswith("xl/"):
                        normalized = f"xl/{normalized.lstrip('./')}"
                    relationship_targets[rel_id] = normalized

            ordered_sheets: list[tuple[str, str]] = []
            if "xl/workbook.xml" in archive.namelist():
                workbook_root = ElementTree.fromstring(archive.read("xl/workbook.xml"))
                for index, node in enumerate(workbook_root.iter()):
                    if not node.tag.endswith("sheet"):
                        continue
                    sheet_name = node.attrib.get("name") or f"Sheet {index + 1}"
                    relationship_id = next(
                        (value for key, value in node.attrib.items() if key.endswith("}id")),
                        None,
                    )
                    target = relationship_targets.get(relationship_id or "")
                    if target:
                        ordered_sheets.append((sheet_name, target))

            if not ordered_sheets:
                ordered_sheets = [
                    (f"Sheet {index + 1}", sheet_file)
                    for index, sheet_file in enumerate(sheet_files)
                ]

            preview_sheets: list[tuple[str, list[list[str]]]] = []
            for sheet_name, target in ordered_sheets:
                if target not in archive.namelist():
                    continue

                sheet_root = ElementTree.fromstring(archive.read(target))
                rows: list[list[str]] = []
                for row in sheet_root.iter():
                    if not row.tag.endswith("}row"):
                        continue
                    row_values: list[str] = []
                    for cell in row:
                        if not cell.tag.endswith("}c"):
                            continue
                        cell_type = cell.attrib.get("t")
                        value_node = next((item for item in cell if item.tag.endswith("}v")), None)

                        if cell_type == "inlineStr":
                            text_node = next(
                                (item for item in cell.iter() if item.tag.endswith("}t")),
                                None,
                            )
                            row_values.append(
                                (text_node.text or "").strip() if text_node is not None else ""
                            )
                            continue

                        if value_node is None or value_node.text is None:
                            row_values.append("")
                            continue

                        raw_value = value_node.text.strip()
                        if cell_type == "s" and raw_value.isdigit():
                            index = int(raw_value)
                            row_values.append(
                                shared_strings[index] if index < len(shared_strings) else raw_value
                            )
                        else:
                            row_values.append(raw_value)
                    if row_values:
                        rows.append(row_values)

                preview_sheets.append((sheet_name, rows))

            return preview_sheets
    except (BadZipFile, ElementTree.ParseError, KeyError, ValueError):
        return []


def _extract_pptx_slides(file_path: Path) -> list[list[str]]:
    try:
        with ZipFile(file_path) as archive:
            slide_names = sorted(
                name
                for name in archive.namelist()
                if re.fullmatch(r"ppt/slides/slide\d+\.xml", name)
            )
            slides: list[list[str]] = []
            for slide_name in slide_names:
                root = ElementTree.fromstring(archive.read(slide_name))
                texts = [
                    node.text.strip()
                    for node in root.iter()
                    if node.tag.endswith("}t") and node.text
                ]
                slides.append(texts)
            return slides
    except (BadZipFile, ElementTree.ParseError, KeyError):
        return []


def _build_xlsx_markdown(
    *,
    original_name: str,
    sheets: list[tuple[str, list[list[str]]]],
) -> str:
    if not sheets:
        return ""

    parts = [f"# Excel 工作簿：{original_name}"]
    for sheet_index, (sheet_name, rows) in enumerate(sheets[:MAX_EXTRACTED_XLSX_SHEETS], start=1):
        visible_rows = [
            [_escape_markdown_cell(cell) for cell in row[:MAX_EXTRACTED_XLSX_COLUMNS]]
            for row in rows[:MAX_EXTRACTED_XLSX_ROWS_PER_SHEET]
            if any(str(cell or "").strip() for cell in row)
        ]
        if not visible_rows:
            continue

        column_count = max(len(row) for row in visible_rows)
        normalized_rows = [row + [""] * (column_count - len(row)) for row in visible_rows]
        header = [
            cell or f"列{column_index + 1}" for column_index, cell in enumerate(normalized_rows[0])
        ]

        parts.append(f"## 工作表 {sheet_index}：{sheet_name}")
        parts.append(_format_markdown_table(header, normalized_rows[1:]))
        parts.append("### 行级检索线索")
        for row_number, row in enumerate(normalized_rows[1:], start=2):
            pairs = [
                f"{header[column_index]}={value}" for column_index, value in enumerate(row) if value
            ]
            if pairs:
                parts.append(f"- {sheet_name} 第 {row_number} 行：" + "；".join(pairs))

        if len(rows) > MAX_EXTRACTED_XLSX_ROWS_PER_SHEET:
            parts.append(
                f"- {sheet_name} 还有 {len(rows) - MAX_EXTRACTED_XLSX_ROWS_PER_SHEET} 行未展开。"
            )

    return "\n\n".join(part for part in parts if part).strip()


def _build_pptx_markdown(
    *,
    original_name: str,
    slides: list[list[str]],
) -> str:
    if not slides:
        return ""

    parts = [f"# PowerPoint 演示文稿：{original_name}"]
    for slide_index, slide_lines in enumerate(slides[:MAX_EXTRACTED_PPTX_SLIDES], start=1):
        lines = [line.strip() for line in slide_lines if str(line or "").strip()]
        if not lines:
            continue
        parts.append(f"## 幻灯片 {slide_index}")
        parts.extend(f"- {line}" for line in lines)
    if len(slides) > MAX_EXTRACTED_PPTX_SLIDES:
        parts.append(f"- 还有 {len(slides) - MAX_EXTRACTED_PPTX_SLIDES} 页未展开。")
    return "\n\n".join(part for part in parts if part).strip()


def _format_markdown_table(header: list[str], rows: list[list[str]]) -> str:
    table_rows = [header] + rows
    separator = ["---"] * len(header)
    lines = [
        "| " + " | ".join(table_rows[0]) + " |",
        "| " + " | ".join(separator) + " |",
    ]
    lines.extend("| " + " | ".join(row) + " |" for row in table_rows[1:])
    return "\n".join(lines)


def _escape_markdown_cell(value: str) -> str:
    text = str(value or "").replace("\r\n", " ").replace("\r", " ").replace("\n", " ")
    text = re.sub(r"\s+", " ", text).strip()
    return text.replace("|", "\\|")
