from __future__ import annotations

from typing import Any

from app.models.enums import SourceType
from app.models.stored_embedding_record import StoredEmbeddingRecord
from app.models.upstream.code_chunk import CodeChunkDocument
from app.models.upstream.commit_analysis import CommitAnalysisDocument
from app.services.commit_embed_text import build_commit_embed_text
from app.models.upstream.doc_chunk import DocChunkDocument
from app.repositories import embedding_record_repo
from app.repositories.upstream import code_chunk_repo, commit_analysis_repo, doc_chunk_repo
from app.retrieval.models import HydratedHit, HydrationResult, HydrationSource, VectorHit


def _upstream_id_from_record_id(record_id: str) -> str:
    normalized = record_id.strip()
    if normalized.startswith("emb_"):
        return normalized.removeprefix("emb_")
    return normalized


def _metadata_value(record: StoredEmbeddingRecord | None, key: str) -> Any:
    if record is None:
        return None
    metadata = record.metadata
    if hasattr(metadata, key):
        return getattr(metadata, key)
    if isinstance(metadata, dict):
        return metadata.get(key)
    return None


def _non_empty_text(value: str | None) -> str | None:
    if value is None:
        return None
    text = value.strip()
    return text or None


def _chunk_lookup_id(
    vector_hit: VectorHit,
    stored: StoredEmbeddingRecord | None,
) -> str:
    if stored and stored.upstream_chunk_id:
        return stored.upstream_chunk_id
    return _upstream_id_from_record_id(vector_hit.record_id)


def _analysis_lookup_id(
    vector_hit: VectorHit,
    stored: StoredEmbeddingRecord | None,
) -> str:
    if stored and stored.upstream_analysis_id:
        return stored.upstream_analysis_id
    return _upstream_id_from_record_id(vector_hit.record_id)


def _git_metadata_from_record(record: StoredEmbeddingRecord | None) -> dict[str, str | None]:
    if record is None:
        return {}
    return {
        "author_name": _metadata_value(record, "author_name"),
        "author_email": _metadata_value(record, "author_email"),
        "committer_name": _metadata_value(record, "committer_name"),
        "committer_email": _metadata_value(record, "committer_email"),
        "authored_at": _metadata_value(record, "authored_at"),
        "committed_at": _metadata_value(record, "committed_at"),
        "commit_message": _metadata_value(record, "commit_message"),
    }


def _git_metadata_from_analysis(analysis: CommitAnalysisDocument) -> dict[str, str | None]:
    return {
        "author_name": analysis.author_name,
        "author_email": analysis.author_email,
        "committer_name": analysis.committer_name,
        "committer_email": analysis.committer_email,
        "authored_at": analysis.authored_at,
        "committed_at": analysis.committed_at,
        "commit_message": analysis.commit_message,
    }


def _from_embedding_record(vector_hit: VectorHit, record: StoredEmbeddingRecord) -> HydratedHit:
    git_meta = _git_metadata_from_record(record)
    return HydratedHit(
        record_id=vector_hit.record_id,
        source_type=vector_hit.source_type,
        score=vector_hit.score,
        chunk_type=vector_hit.chunk_type or record.chunk_type.value,
        text=record.text.strip(),
        file_path=record.file_path or vector_hit.file_path,
        doc_path=record.doc_path or vector_hit.doc_path,
        section_title=record.section_title or vector_hit.section_title,
        symbol_name=record.symbol_name or vector_hit.symbol_name,
        symbol_type=_metadata_value(record, "symbol_type"),
        language=_metadata_value(record, "language"),
        section_level=_metadata_value(record, "section_level"),
        start_line=record.start_line,
        end_line=record.end_line,
        commit_sha=record.commit_sha or vector_hit.commit_sha,
        graph_node_id=record.graph_node_id or vector_hit.graph_node_id,
        impacted_symbols=list(_metadata_value(record, "impacted_symbols") or []),
        changed_files=list(_metadata_value(record, "changed_files") or []),
        author_name=git_meta["author_name"],
        author_email=git_meta["author_email"],
        committer_name=git_meta["committer_name"],
        committer_email=git_meta["committer_email"],
        authored_at=git_meta["authored_at"],
        committed_at=git_meta["committed_at"],
        commit_message=git_meta["commit_message"],
        hydration_source=HydrationSource.EMBEDDING_RECORD,
    )


def _from_code_chunk(
    vector_hit: VectorHit,
    chunk: CodeChunkDocument,
) -> HydratedHit:
    text = _non_empty_text(chunk.text)
    if not text:
        raise ValueError("code chunk text is empty")
    return HydratedHit(
        record_id=vector_hit.record_id,
        source_type=SourceType.CODE.value,
        score=vector_hit.score,
        chunk_type=vector_hit.chunk_type or "symbol",
        text=text,
        file_path=chunk.file_path,
        symbol_name=chunk.symbol_name,
        symbol_type=chunk.symbol_type,
        language=chunk.language,
        start_line=chunk.start_line,
        end_line=chunk.end_line,
        graph_node_id=chunk.graph_node_id or vector_hit.graph_node_id,
        hydration_source=HydrationSource.CODE_CHUNK,
    )


def _from_doc_chunk(
    vector_hit: VectorHit,
    chunk: DocChunkDocument,
) -> HydratedHit:
    text = _non_empty_text(chunk.chunk_text)
    if not text:
        raise ValueError("doc chunk text is empty")
    return HydratedHit(
        record_id=vector_hit.record_id,
        source_type=SourceType.DOCS.value,
        score=vector_hit.score,
        chunk_type=vector_hit.chunk_type or "text",
        text=text,
        doc_path=chunk.doc_path,
        section_title=chunk.section_title,
        section_level=chunk.resolved_section_level(),
        graph_node_id=vector_hit.graph_node_id,
        hydration_source=HydrationSource.DOC_CHUNK,
    )


def _from_commit_analysis(
    vector_hit: VectorHit,
    analysis: CommitAnalysisDocument,
) -> HydratedHit:
    text = _non_empty_text(build_commit_embed_text(analysis))
    if not text:
        raise ValueError("commit analysis text is empty")
    git_meta = _git_metadata_from_analysis(analysis)
    return HydratedHit(
        record_id=vector_hit.record_id,
        source_type=SourceType.COMMIT.value,
        score=vector_hit.score,
        chunk_type=vector_hit.chunk_type or "summary",
        text=text,
        commit_sha=analysis.commit_sha,
        impacted_symbols=list(analysis.impacted_symbols),
        changed_files=list(analysis.changed_files),
        author_name=git_meta["author_name"],
        author_email=git_meta["author_email"],
        committer_name=git_meta["committer_name"],
        committer_email=git_meta["committer_email"],
        authored_at=git_meta["authored_at"],
        committed_at=git_meta["committed_at"],
        commit_message=git_meta["commit_message"],
        hydration_source=HydrationSource.COMMIT_ANALYSIS,
    )


async def hydrate_vector_hits(vector_hits: list[VectorHit]) -> HydrationResult:
    """
    Batch-hydrate vector hits from embedding_records with upstream fallbacks.
    """
    retrieval_count = len(vector_hits)
    if not vector_hits:
        return HydrationResult(
            retrieval_count=0,
            hydrated_count=0,
            skipped_count=0,
        )

    record_ids = [hit.record_id for hit in vector_hits]
    records_by_id = await embedding_record_repo.get_records_by_ids(record_ids)

    pending_code: list[tuple[VectorHit, StoredEmbeddingRecord | None, str]] = []
    pending_docs: list[tuple[VectorHit, StoredEmbeddingRecord | None, str]] = []
    pending_commit: list[tuple[VectorHit, StoredEmbeddingRecord | None, str]] = []
    hydrated: list[HydratedHit] = []

    for vector_hit in vector_hits:
        stored = records_by_id.get(vector_hit.record_id)
        primary_text = _non_empty_text(stored.text if stored else None)
        if stored is not None and primary_text:
            hydrated.append(_from_embedding_record(vector_hit, stored))
            continue

        source_type = vector_hit.source_type
        if source_type == SourceType.CODE.value:
            pending_code.append(
                (vector_hit, stored, _chunk_lookup_id(vector_hit, stored))
            )
        elif source_type == SourceType.DOCS.value:
            pending_docs.append(
                (vector_hit, stored, _chunk_lookup_id(vector_hit, stored))
            )
        elif source_type == SourceType.COMMIT.value:
            pending_commit.append(
                (vector_hit, stored, _analysis_lookup_id(vector_hit, stored))
            )
        else:
            continue

    if pending_code:
        chunks = await code_chunk_repo.get_by_chunk_ids(
            [chunk_id for _, _, chunk_id in pending_code]
        )
        for vector_hit, _stored, chunk_id in pending_code:
            chunk = chunks.get(chunk_id)
            if chunk is None:
                continue
            try:
                hydrated.append(_from_code_chunk(vector_hit, chunk))
            except ValueError:
                continue

    if pending_docs:
        chunks = await doc_chunk_repo.get_by_chunk_ids(
            [chunk_id for _, _, chunk_id in pending_docs]
        )
        for vector_hit, _stored, chunk_id in pending_docs:
            chunk = chunks.get(chunk_id)
            if chunk is None:
                continue
            try:
                hydrated.append(_from_doc_chunk(vector_hit, chunk))
            except ValueError:
                continue

    if pending_commit:
        analyses = await commit_analysis_repo.get_by_analysis_ids(
            [analysis_id for _, _, analysis_id in pending_commit]
        )
        for vector_hit, _stored, analysis_id in pending_commit:
            analysis = analyses.get(analysis_id)
            if analysis is None:
                continue
            try:
                hydrated.append(_from_commit_analysis(vector_hit, analysis))
            except ValueError:
                continue

    hydrated_count = len(hydrated)
    return HydrationResult(
        hits=hydrated,
        retrieval_count=retrieval_count,
        hydrated_count=hydrated_count,
        skipped_count=retrieval_count - hydrated_count,
    )
