from __future__ import annotations

from collections.abc import Callable
from typing import Any

from backend.config import get_extraction_mode
from backend.constants import DOCX_MIME_TYPE, PDF_MIME_TYPE, PPTX_MIME_TYPE
from backend.services.artifact_models import ReadArtifactResult
from backend.services.artifact_read_service.docx_content import build_docx_llm_readable_content
from backend.services.artifact_read_service.inline_images import (
    extract_inline_image_candidates,
    filter_sendable_inline_image_parts,
    image_read_status,
)
from backend.services.artifact_read_service.pdf_content import build_pdf_llm_readable_content
from backend.services.generated_document.generated_document_metadata import extract_pptx_deck_yaml_from_artifact

_MAX_RECORD_ROWS = 20
_MAX_RECORD_COLUMNS = 10
_LLM_READABLE_CONTENT_KEY = "llmReadableContent"


def read_artifact_payload(
    *,
    artifact: dict[str, Any],
    message_id: str,
    artifacts_id: str,
    artifact_index: int,
    format_unsupported: Callable[..., str],
) -> ReadArtifactResult:
    """Route one persisted artifact to the reader that can turn it into LLM-friendly content."""

    # Keep the dispatch explicit so unsupported artifact families fail deterministically for the orchestrator.
    if _contains_agent_hub_pptx(artifact):
        return _read_pptx_artifact(
            artifact=artifact,
            message_id=message_id,
            artifacts_id=artifacts_id,
            artifact_index=artifact_index,
            format_unsupported=format_unsupported,
        )

    if _contains_records(artifact):
        return _read_records_artifact(
            artifact=artifact,
            message_id=message_id,
            artifacts_id=artifacts_id,
            artifact_index=artifact_index,
        )

    if extract_inline_image_candidates(artifact):
        return _read_inline_image_artifact(
            artifact=artifact,
            message_id=message_id,
            artifacts_id=artifacts_id,
            artifact_index=artifact_index,
        )

    if _contains_markdown(artifact):
        return _read_markdown_artifact(
            artifact=artifact,
            message_id=message_id,
            artifacts_id=artifacts_id,
            artifact_index=artifact_index,
        )

    if _find_inline_pdf_part(artifact) is not None:
        return _read_pdf_artifact(
            artifact=artifact,
            message_id=message_id,
            artifacts_id=artifacts_id,
            artifact_index=artifact_index,
        )

    if _find_inline_docx_part(artifact) is not None:
        return _read_docx_artifact(
            artifact=artifact,
            message_id=message_id,
            artifacts_id=artifacts_id,
            artifact_index=artifact_index,
        )

    artifact_name = _get_artifact_name(artifact)
    return ReadArtifactResult(
        result=format_unsupported(
            artifact=artifact,
            message_id=message_id,
            artifacts_id=artifacts_id,
            artifact_index=artifact_index,
            reason="artifact_type_not_supported",
        ),
        status="unsupported",
        read_mode="unsupported",
        artifact_name=artifact_name,
    )


# Artifact family detection


def _contains_agent_hub_pptx(artifact: dict[str, Any]) -> bool:
    """Return whether the artifact should be treated as an Agent Hub PPTX artifact."""

    if extract_pptx_deck_yaml_from_artifact(artifact) is not None:
        return True

    for part in artifact.get("parts") or []:
        if not isinstance(part, dict):
            continue
        filename = part.get("filename")
        if isinstance(filename, str) and filename.strip().lower().endswith(".pptx"):
            return True
        if part.get("mimeType") == PPTX_MIME_TYPE:
            return True
    return False


def _contains_records(artifact: dict[str, Any]) -> bool:
    """Return whether the artifact exposes tabular RECORDS content."""

    for part in artifact.get("parts") or []:
        if isinstance(part, dict) and part.get("type") == "RECORDS":
            return True
    return False


def _contains_markdown(artifact: dict[str, Any]) -> bool:
    """Return whether the artifact exposes a markdown snippet."""

    for part in artifact.get("parts") or []:
        if isinstance(part, dict) and part.get("type") == "SIMPLE_DOCUMENT" and part.get("markdownSnippet"):
            return True
    return False


# Artifact readers


def _read_pptx_artifact(
    *,
    artifact: dict[str, Any],
    message_id: str,
    artifacts_id: str,
    artifact_index: int,
    format_unsupported: Callable[..., str],
) -> ReadArtifactResult:
    """Read a PPTX artifact by exposing the embedded deck YAML to the LLM."""

    deck_yaml = extract_pptx_deck_yaml_from_artifact(artifact)
    if deck_yaml is None:
        artifact_name = _get_artifact_name(artifact)
        return ReadArtifactResult(
            result=format_unsupported(
                artifact=artifact,
                message_id=message_id,
                artifacts_id=artifacts_id,
                artifact_index=artifact_index,
                reason="pptx_without_embedded_yaml",
            ),
            status="unsupported",
            read_mode="unsupported",
            artifact_name=artifact_name,
        )

    lines = _build_header_lines(
        artifact=artifact,
        message_id=message_id,
        artifacts_id=artifacts_id,
        artifact_index=artifact_index,
        status="ok",
        read_mode="pptx_deck_yaml",
    )
    lines.extend(["content:", deck_yaml])
    artifact_name = _get_artifact_name(artifact)
    return ReadArtifactResult(
        result="\n".join(lines),
        status="ok",
        read_mode="pptx_deck_yaml",
        artifact_name=artifact_name,
        content_text=deck_yaml,
    )


def _read_records_artifact(
    *, artifact: dict[str, Any], message_id: str, artifacts_id: str, artifact_index: int
) -> ReadArtifactResult:
    """Read a records artifact as a clipped textual preview."""

    content_lines: list[str] = []
    for index, part in enumerate(artifact.get("parts") or []):
        content_lines.extend(_build_records_part_lines(index=index, part=part))

    lines = _build_header_lines(
        artifact=artifact,
        message_id=message_id,
        artifacts_id=artifacts_id,
        artifact_index=artifact_index,
        status="ok",
        read_mode="records_preview",
    )
    lines.extend(content_lines)
    artifact_name = _get_artifact_name(artifact)
    records_content = "\n".join(content_lines)
    return ReadArtifactResult(
        result="\n".join(lines),
        status="ok",
        read_mode="records_preview",
        artifact_name=artifact_name,
        content_text=records_content,
    )


def _read_markdown_artifact(
    *, artifact: dict[str, Any], message_id: str, artifacts_id: str, artifact_index: int
) -> ReadArtifactResult:
    """Read markdown snippets from SIMPLE_DOCUMENT parts."""

    content_lines: list[str] = []
    for index, part in enumerate(artifact.get("parts") or []):
        if isinstance(part, dict) and part.get("type") == "SIMPLE_DOCUMENT" and part.get("markdownSnippet"):
            markdown_content = part.get("markdownSnippet")
            content_lines.extend(
                [
                    f"markdown_part_{index}:",
                    "  content:",
                    str(markdown_content),
                ]
            )

    lines = _build_header_lines(
        artifact=artifact,
        message_id=message_id,
        artifacts_id=artifacts_id,
        artifact_index=artifact_index,
        status="ok",
        read_mode="markdown_preview",
    )
    lines.extend(content_lines)
    artifact_name = _get_artifact_name(artifact)
    markdown_content = "\n".join(content_lines)
    return ReadArtifactResult(
        result="\n".join(lines),
        status="ok",
        read_mode="markdown_preview",
        artifact_name=artifact_name,
        content_text=markdown_content,
    )


def _read_inline_image_artifact(
    *, artifact: dict[str, Any], message_id: str, artifacts_id: str, artifact_index: int
) -> ReadArtifactResult:
    """Read an inline-image artifact as text diagnostics plus multimodal image parts."""

    image_parts = extract_inline_image_candidates(artifact)
    # Split image parts between those we can attach to the LLM and those we only list as skipped in the text result.
    sendable_parts, skipped_parts = filter_sendable_inline_image_parts(image_parts)
    status = image_read_status(image_parts=image_parts, sendable_parts=sendable_parts, skipped_parts=skipped_parts)
    lines = _build_header_lines(
        artifact=artifact,
        message_id=message_id,
        artifacts_id=artifacts_id,
        artifact_index=artifact_index,
        status=status,
        read_mode="inline_image",
    )
    lines.append(f"- image_count: {len(image_parts)}")
    lines.append(f"- attached_image_count: {len(sendable_parts)}")
    if skipped_parts:
        lines.append(f"- skipped_image_count: {len(skipped_parts)}")
    if not sendable_parts and image_parts:
        lines.append("- note: no image could be attached for visual inspection")

    # Keep one text block per source image so the LLM can reason about what was attached or skipped.
    content_lines: list[str] = []
    skipped_reasons_by_index = {item["index"]: item["reason"] for item in skipped_parts}
    for index, part in enumerate(image_parts):
        filename = part.get("filename") if isinstance(part.get("filename"), str) else ""
        mime_type = part.get("mimeType") if isinstance(part.get("mimeType"), str) else ""
        part_lines = [f"image_part_{index}:"]
        if filename:
            part_lines.append(f"  filename: {filename}")
        if mime_type:
            part_lines.append(f"  mimeType: {mime_type}")
        skipped_reason = skipped_reasons_by_index.get(index)
        if skipped_reason is None:
            part_lines.append("  content: attached_for_visual_inspection")
        else:
            part_lines.append("  content: not_attached")
            part_lines.append(f"  reason: {skipped_reason}")
        lines.extend(part_lines)
        content_lines.extend(part_lines)

    artifact_name = _get_artifact_name(artifact)
    return ReadArtifactResult(
        result="\n".join(lines),
        parts=[
            {
                "type": "IMAGE_INLINE",
                "inlineImage": part["dataBase64"],
                "imageMimeType": part["mimeType"],
            }
            for part in sendable_parts
        ],
        status=status,
        read_mode="inline_image",
        artifact_name=artifact_name,
        content_text="\n".join(content_lines),
    )


def _read_pdf_artifact(
    *, artifact: dict[str, Any], message_id: str, artifacts_id: str, artifact_index: int
) -> ReadArtifactResult:
    """Read an inline PDF artifact using cached or freshly generated LLM-readable content."""

    pdf_part = _find_inline_pdf_part(artifact)
    if pdf_part is None:
        artifact_name = _get_artifact_name(artifact)
        return ReadArtifactResult(
            result=_format_llm_readable_content_failure(
                artifact=artifact,
                message_id=message_id,
                artifacts_id=artifacts_id,
                artifact_index=artifact_index,
                read_mode="pdf_llm_readable_content",
                reason="pdf_part_not_found",
            ),
            status="unsupported",
            read_mode="pdf_llm_readable_content",
            artifact_name=artifact_name,
        )

    extraction_mode = get_extraction_mode()
    llm_readable_content = _normalize_pdf_llm_readable_content(
        pdf_part.get(_LLM_READABLE_CONTENT_KEY),
        extraction_mode=extraction_mode,
    )
    if llm_readable_content is None:
        try:
            llm_readable_content = build_pdf_llm_readable_content(part=pdf_part, extraction_mode=extraction_mode)
        except Exception as err:
            artifact_name = _get_artifact_name(artifact)
            return ReadArtifactResult(
                result=_format_llm_readable_content_failure(
                    artifact=artifact,
                    message_id=message_id,
                    artifacts_id=artifacts_id,
                    artifact_index=artifact_index,
                    read_mode="pdf_llm_readable_content",
                    reason=f"pdf_llm_readable_content_generation_failed: {err}",
                ),
                status="unsupported",
                read_mode="pdf_llm_readable_content",
                artifact_name=artifact_name,
            )
        pdf_part[_LLM_READABLE_CONTENT_KEY] = llm_readable_content

    lines = _build_header_lines(
        artifact=artifact,
        message_id=message_id,
        artifacts_id=artifacts_id,
        artifact_index=artifact_index,
        status="ok",
        read_mode="pdf_llm_readable_content",
    )
    images = llm_readable_content.get("images") or []
    text = llm_readable_content.get("text") or ""
    if images:
        lines.append(f"- attached_image_count: {len(images)}")
    if text:
        lines.extend(["content:", text])

    artifact_name = _get_artifact_name(artifact)
    return ReadArtifactResult(
        result="\n".join(lines),
        parts=[
            {
                "type": "IMAGE_INLINE",
                "inlineImage": image["dataBase64"],
                "imageMimeType": image["mimeType"],
            }
            for image in images
        ],
        status="ok",
        read_mode="pdf_llm_readable_content",
        artifact_name=artifact_name,
        content_text=text,
    )


def _read_docx_artifact(
    *, artifact: dict[str, Any], message_id: str, artifacts_id: str, artifact_index: int
) -> ReadArtifactResult:
    """Read an inline DOCX artifact using cached or freshly generated LLM-readable text."""

    docx_part = _find_inline_docx_part(artifact)
    if docx_part is None:
        artifact_name = _get_artifact_name(artifact)
        return ReadArtifactResult(
            result=_format_llm_readable_content_failure(
                artifact=artifact,
                message_id=message_id,
                artifacts_id=artifacts_id,
                artifact_index=artifact_index,
                read_mode="docx_llm_readable_content",
                reason="docx_part_not_found",
            ),
            status="unsupported",
            read_mode="docx_llm_readable_content",
            artifact_name=artifact_name,
        )

    llm_readable_content = _normalize_docx_llm_readable_content(docx_part.get(_LLM_READABLE_CONTENT_KEY))
    if llm_readable_content is None:
        try:
            llm_readable_content = build_docx_llm_readable_content(part=docx_part)
        except Exception as err:
            artifact_name = _get_artifact_name(artifact)
            return ReadArtifactResult(
                result=_format_llm_readable_content_failure(
                    artifact=artifact,
                    message_id=message_id,
                    artifacts_id=artifacts_id,
                    artifact_index=artifact_index,
                    read_mode="docx_llm_readable_content",
                    reason=f"docx_llm_readable_content_generation_failed: {err}",
                ),
                status="unsupported",
                read_mode="docx_llm_readable_content",
                artifact_name=artifact_name,
            )
        docx_part[_LLM_READABLE_CONTENT_KEY] = llm_readable_content

    lines = _build_header_lines(
        artifact=artifact,
        message_id=message_id,
        artifacts_id=artifacts_id,
        artifact_index=artifact_index,
        status="ok",
        read_mode="docx_llm_readable_content",
    )
    text = llm_readable_content.get("text") or ""
    if text:
        lines.extend(["content:", text])

    artifact_name = _get_artifact_name(artifact)
    return ReadArtifactResult(
        result="\n".join(lines),
        status="ok",
        read_mode="docx_llm_readable_content",
        artifact_name=artifact_name,
        content_text=text,
    )


# Text result builders


def _build_records_part_lines(*, index: int, part: Any) -> list[str]:
    """Render one RECORDS part into a clipped textual preview."""

    if not isinstance(part, dict) or part.get("type") != "RECORDS":
        return []

    records = part.get("records") or {}
    columns = records.get("columns") if isinstance(records.get("columns"), list) else []
    data = records.get("data") if isinstance(records.get("data"), list) else []
    # Clip large tabular payloads so the tool stays usable inside the orchestrator prompt budget.
    clipped_columns = [str(column) for column in columns[:_MAX_RECORD_COLUMNS]]
    clipped_rows = data[:_MAX_RECORD_ROWS]

    lines = [
        f"records_part_{index}:",
        f"  columns: {clipped_columns}",
        f"  row_count: {len(data)}",
    ]
    if len(columns) > _MAX_RECORD_COLUMNS:
        lines.append(f"  note: columns clipped to first {_MAX_RECORD_COLUMNS}")
    if len(data) > _MAX_RECORD_ROWS:
        lines.append(f"  note: rows clipped to first {_MAX_RECORD_ROWS}")
    lines.append("  rows:")
    lines.extend(_build_records_row_lines(clipped_rows))
    return lines


def _build_records_row_lines(rows: list[Any]) -> list[str]:
    """Render preview rows into a compact YAML-like bullet list."""

    lines: list[str] = []
    for row in rows:
        normalized_row = row if isinstance(row, list) else [row]
        lines.append(f"    - {[str(cell) for cell in normalized_row[:_MAX_RECORD_COLUMNS]]}")
    return lines


def _build_header_lines(
    *,
    artifact: dict[str, Any],
    message_id: str,
    artifacts_id: str,
    artifact_index: int,
    status: str,
    read_mode: str,
) -> list[str]:
    """Build the common header shared by every read_artifact textual result."""

    artifact_name = artifact.get("name") if isinstance(artifact.get("name"), str) else ""
    artifact_type = artifact.get("type") if isinstance(artifact.get("type"), str) else "unknown"
    return [
        "Artifact read result:",
        f"- status: {status}",
        f"- reference: message_id={message_id}, artifacts_id={artifacts_id}, artifact_index={artifact_index}",
        f"- name: {artifact_name}",
        f"- type: {artifact_type}",
        f"- read_mode: {read_mode}",
    ]


def _get_artifact_name(artifact: dict[str, Any]) -> str:
    """Return the human-readable artifact name when present."""

    return artifact.get("name") if isinstance(artifact.get("name"), str) else ""


def _find_inline_pdf_part(artifact: dict[str, Any]) -> dict[str, Any] | None:
    """Return the first inline PDF part from the artifact."""

    for part in artifact.get("parts") or []:
        if not isinstance(part, dict) or part.get("type") != "DATA_INLINE":
            continue
        filename = part.get("filename")
        if isinstance(filename, str) and filename.strip().lower().endswith(".pdf"):
            return part
        mime_type = part.get("mimeType")
        if isinstance(mime_type, str) and mime_type.strip().lower() == PDF_MIME_TYPE:
            return part
    return None


def _find_inline_docx_part(artifact: dict[str, Any]) -> dict[str, Any] | None:
    """Return the first inline DOCX part from the artifact."""

    for part in artifact.get("parts") or []:
        if not isinstance(part, dict) or part.get("type") != "DATA_INLINE":
            continue
        filename = part.get("filename")
        if isinstance(filename, str) and filename.strip().lower().endswith(".docx"):
            return part
        mime_type = part.get("mimeType")
        if isinstance(mime_type, str) and mime_type.strip().lower() == DOCX_MIME_TYPE:
            return part
    return None


def _normalize_pdf_llm_readable_content(value: Any, *, extraction_mode: str) -> dict[str, Any] | None:
    """Keep only the cached PDF content relevant to the current admin extraction mode."""

    if not isinstance(value, dict):
        return None

    if extraction_mode == "pagesText":
        text = value.get("text")
        if isinstance(text, str) and text.strip():
            return {"text": text}
        return None

    if extraction_mode == "pagesScreenshots":
        images = value.get("images")
        if not isinstance(images, list):
            return None

        normalized_images = []
        for image in images:
            if not isinstance(image, dict):
                continue
            mime_type = image.get("mimeType")
            data_base64 = image.get("dataBase64")
            if not isinstance(mime_type, str) or not mime_type.strip():
                continue
            if not isinstance(data_base64, str) or not data_base64.strip():
                continue
            normalized_image = {
                "mimeType": mime_type,
                "dataBase64": data_base64,
            }
            page_number = image.get("pageNumber")
            if isinstance(page_number, int) and page_number > 0:
                normalized_image["pageNumber"] = page_number
            normalized_images.append(normalized_image)

        if normalized_images:
            return {"images": normalized_images}
        return None

    return None


def _normalize_docx_llm_readable_content(value: Any) -> dict[str, Any] | None:
    """Keep only cached DOCX text content that is still usable for the current read."""

    if not isinstance(value, dict):
        return None

    text = value.get("text")
    if isinstance(text, str) and text.strip():
        return {"text": text}
    return None


def _format_llm_readable_content_failure(
    *,
    artifact: dict[str, Any],
    message_id: str,
    artifacts_id: str,
    artifact_index: int,
    read_mode: str,
    reason: str,
) -> str:
    """Format a stable read failure result for generated LLM-readable artifact content."""

    lines = _build_header_lines(
        artifact=artifact,
        message_id=message_id,
        artifacts_id=artifacts_id,
        artifact_index=artifact_index,
        status="unsupported",
        read_mode=read_mode,
    )
    lines.append(f"- reason: {reason}")
    return "\n".join(lines)
