""" Isnad chain endpoints — chain visualization data for hadith detail views. """ from fastapi import APIRouter, Query, HTTPException from app.services.database import db from app.models.schemas import IsnadChain, IsnadNode, IsnadLink router = APIRouter(prefix="/chains", tags=["Isnad Chains"]) @router.get("/hadith/{hadith_id}", response_model=IsnadChain, summary="Get isnad chain for a hadith", description="Returns the complete isnad (chain of narration) as a graph structure " "with nodes (narrators) and links (transmission relationships). " "Ready for visualization with D3.js, vis.js, Cytoscape.js, or any graph library. " "Each node includes narrator metadata (generation, reliability); " "each link includes the transmission verb (حدثنا، عن، أخبرنا).") async def get_isnad_chain(hadith_id: str): """ Get the full isnad chain for a hadith as a graph (nodes + links) ready for visualization (D3.js, vis.js, etc.). """ # Get hadith info hadith = db.neo4j_query_one(""" MATCH (h:Hadith {id: $hid}) RETURN h.id AS id, h.collection AS collection, h.hadith_number AS hadith_number """, {"hid": hadith_id}) if not hadith: raise HTTPException(status_code=404, detail="Hadith not found in graph") # Get chain nodes nodes = db.neo4j_query(""" MATCH (n:Narrator)-[r:APPEARS_IN]->(h:Hadith {id: $hid}) RETURN n.name_arabic AS name_arabic, n.name_transliterated AS name_transliterated, n.entity_type AS entity_type, n.generation AS generation, n.reliability_grade AS reliability_grade, r.chain_order AS chain_order ORDER BY r.chain_order """, {"hid": hadith_id}) # Get chain links (NARRATED_FROM within this hadith's narrators) links = db.neo4j_query(""" MATCH (a:Narrator)-[r1:APPEARS_IN]->(h:Hadith {id: $hid}) MATCH (b:Narrator)-[r2:APPEARS_IN]->(h) MATCH (a)-[nf:NARRATED_FROM]->(b) WHERE $hid IN nf.hadith_ids RETURN a.name_arabic AS source, b.name_arabic AS target, nf.transmission_verb AS transmission_verb """, {"hid": hadith_id}) # If no NARRATED_FROM edges with hadith_id, fall back to chain order if not links and len(nodes) > 1: sorted_nodes = sorted(nodes, key=lambda n: n.get("chain_order") or 999) links = [] for i in range(len(sorted_nodes) - 1): links.append({ "source": sorted_nodes[i]["name_arabic"], "target": sorted_nodes[i + 1]["name_arabic"], "transmission_verb": None, }) return IsnadChain( hadith_id=str(hadith["id"]), collection=hadith["collection"] or "", hadith_number=hadith["hadith_number"] or 0, nodes=[IsnadNode(**n) for n in nodes], links=[IsnadLink(**l) for l in links], ) @router.get("/narrator/{name_arabic}", response_model=list[IsnadChain], summary="Get all chains for a narrator", description="Returns all isnad chains that include a specific narrator. " "Useful for visualizing how a narrator connects to the Prophet ﷺ " "through different transmission paths. " "Example: `/chains/narrator/الزهري`") async def get_narrator_chains( name_arabic: str, limit: int = Query(10, ge=1, le=50, description="Maximum chains to return"), ): """ Get all isnad chains that include a specific narrator. Useful for seeing how a narrator connects to the Prophet ﷺ. """ hadith_ids = db.neo4j_query(""" MATCH (n:Narrator {name_arabic: $name})-[:APPEARS_IN]->(h:Hadith) RETURN h.id AS id LIMIT $limit """, {"name": name_arabic, "limit": limit}) chains = [] for row in hadith_ids: chain = await get_isnad_chain(str(row["id"])) chains.append(chain) return chains @router.get("/common-chains", response_model=list[dict], summary="Find shared chains between two narrators", description="Find hadiths where both narrators appear in the same isnad chain. " "Useful for verifying narrator relationships and finding corroborating chains. " "Example: `/chains/common-chains?narrator_a=الزهري&narrator_b=أنس بن مالك`") async def find_common_chains( narrator_a: str = Query( ..., description="First narrator (Arabic). Example: الزهري", examples=["الزهري"], ), narrator_b: str = Query( ..., description="Second narrator (Arabic). Example: أنس بن مالك", examples=["أنس بن مالك"], ), limit: int = Query(10, ge=1, le=50, description="Maximum results"), ): """ Find hadiths where both narrators appear in the same chain. Useful for verifying narrator relationships. """ rows = db.neo4j_query(""" MATCH (a:Narrator)-[:APPEARS_IN]->(h:Hadith)<-[:APPEARS_IN]-(b:Narrator) WHERE a.name_arabic CONTAINS $name_a AND b.name_arabic CONTAINS $name_b AND a <> b RETURN h.id AS hadith_id, h.collection AS collection, h.hadith_number AS hadith_number, a.name_arabic AS narrator_a, b.name_arabic AS narrator_b LIMIT $limit """, {"name_a": narrator_a, "name_b": narrator_b, "limit": limit}) return [dict(r) for r in rows]