import { i as writeJsonFile, n as readJsonFile } from "./json-store-WWsFzXub.js";
import { t as withJsonFile } from "./file-lock-CcHotQkZ.js";
import { n as logger } from "./logger-Dyl4VcLO.js";
import path from "path";
import fs from "fs";
import matter from "gray-matter";
//#region src/core/graph.ts
function graphPath(dataDir, slug) {
	return path.join(dataDir, "customers", slug, "graph.json");
}
function emptyGraph(slug) {
	return {
		schemaVersion: "1",
		slug,
		nodes: [],
		edges: [],
		updatedAt: (/* @__PURE__ */ new Date()).toISOString()
	};
}
function readGraph(dataDir, slug) {
	const p = graphPath(dataDir, slug);
	if (!fs.existsSync(p)) return emptyGraph(slug);
	try {
		return JSON.parse(fs.readFileSync(p, "utf-8"));
	} catch {
		logger.warn("graph", "failed to parse graph file — returning empty graph", { path: p });
		return emptyGraph(slug);
	}
}
async function writeGraph(dataDir, slug, updater) {
	return withJsonFile(graphPath(dataDir, slug), (current) => {
		return {
			...updater(current),
			updatedAt: (/* @__PURE__ */ new Date()).toISOString()
		};
	});
}
function upsertNode(graph, node) {
	const now = (/* @__PURE__ */ new Date()).toISOString();
	const idx = graph.nodes.findIndex((n) => n.id === node.id);
	if (idx !== -1) {
		const existing = graph.nodes[idx];
		const updated = {
			...existing,
			label: node.label || existing.label,
			properties: {
				...existing.properties,
				...node.properties
			},
			updatedAt: now
		};
		const nodes = [...graph.nodes];
		nodes[idx] = updated;
		return {
			...graph,
			nodes
		};
	}
	const newNode = {
		...node,
		createdAt: now,
		updatedAt: now
	};
	return {
		...graph,
		nodes: [...graph.nodes, newNode]
	};
}
function findNode(graph, id) {
	return graph.nodes.find((n) => n.id === id);
}
function findNodesByType(graph, type) {
	return graph.nodes.filter((n) => n.type === type);
}
function makeEdgeId(type, fromId, toId) {
	return `${type}:${fromId}__${toId}`;
}
function upsertEdge(graph, edge) {
	const id = edge.id ?? makeEdgeId(edge.type, edge.from, edge.to);
	const idx = graph.edges.findIndex((e) => e.id === id);
	if (idx !== -1) {
		const existing = graph.edges[idx];
		const updated = {
			...existing,
			weight: Math.min(1, existing.weight + .05),
			contactCount: existing.contactCount + 1,
			lastContact: edge.lastContact,
			properties: {
				...existing.properties,
				...edge.properties
			}
		};
		const edges = [...graph.edges];
		edges[idx] = updated;
		return {
			...graph,
			edges
		};
	}
	const now = (/* @__PURE__ */ new Date()).toISOString();
	const newEdge = {
		...edge,
		id,
		recordedAt: edge.recordedAt ?? now,
		validFrom: edge.validFrom ?? edge.lastContact ?? now
	};
	return {
		...graph,
		edges: [...graph.edges, newEdge]
	};
}
function getStakeholders(graph) {
	const dedup = (nodes) => {
		const seen = /* @__PURE__ */ new Set();
		return nodes.filter((n) => seen.has(n.id) ? false : (seen.add(n.id), true));
	};
	const resolve = (edges) => dedup(edges.map((e) => findNode(graph, e.from)).filter((n) => n !== void 0));
	const champions = resolve(graph.edges.filter((e) => e.type === "IS_CHAMPION"));
	const blockers = resolve(graph.edges.filter((e) => e.type === "IS_BLOCKER"));
	const economicBuyers = resolve(graph.edges.filter((e) => e.type === "IS_ECONOMIC_BUYER"));
	const allContacts = findNodesByType(graph, "person");
	const missingRoles = [];
	if (allContacts.length > 0) {
		if (champions.length === 0) missingRoles.push({
			role: "champion",
			urgency: "important",
			suggestion: "Identify who is driving this deal internally."
		});
		if (economicBuyers.length === 0) missingRoles.push({
			role: "economic_buyer",
			urgency: "critical",
			suggestion: "Find out who signs the contract. Ask your champion directly."
		});
	}
	return {
		champions,
		blockers,
		economicBuyers,
		allContacts,
		missingRoles
	};
}
/** BFS shortest path between two nodes. Returns [] when no path exists. */
function findPath(graph, fromId, toId) {
	if (fromId === toId) return [fromId];
	const visited = new Set([fromId]);
	const queue = [{
		nodeId: fromId,
		path: [fromId]
	}];
	while (queue.length > 0) {
		const current = queue.shift();
		const neighbors = graph.edges.filter((e) => e.from === current.nodeId || e.to === current.nodeId).map((e) => e.from === current.nodeId ? e.to : e.from).filter((id) => !visited.has(id));
		for (const neighborId of neighbors) {
			const newPath = [...current.path, neighborId];
			if (neighborId === toId) return newPath;
			visited.add(neighborId);
			queue.push({
				nodeId: neighborId,
				path: newPath
			});
		}
	}
	return [];
}
//#endregion
//#region src/core/email-normalizer.ts
function normalizeEmail(raw) {
	if (!raw) return "";
	const trimmed = raw.trim();
	const angleMatch = trimmed.match(/<([^>]+)>/);
	return (angleMatch ? angleMatch[1] : trimmed).toLowerCase().trim();
}
//#endregion
//#region src/core/graph-extractor.ts
function extractEmail(withStr) {
	const angleMatch = withStr.match(/<([^>]+@[^>]+)>/);
	if (angleMatch?.[1]) return angleMatch[1].toLowerCase();
	const bareMatch = withStr.match(/^([^\s]+@[^\s]+)$/);
	if (bareMatch?.[1]) return bareMatch[1].toLowerCase();
}
function extractDisplayName(withStr) {
	const match = withStr.match(/^(.+?)\s*<[^>]+>$/);
	if (match?.[1]) return match[1].trim();
	return withStr.trim();
}
function makePersonId(withStr, slug) {
	const email = normalizeEmail(withStr);
	if (email.includes("@")) return `person:${email}`;
	return `person:${slug}:${extractDisplayName(withStr).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}`;
}
function makeCompanyId(domain, slug, companyName) {
	if (domain) return `company:${domain.toLowerCase()}`;
	if (slug) return `company:${slug}`;
	if (companyName) return `company:${companyName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}`;
	return `company:unknown`;
}
function extractNodes(input) {
	const email = extractEmail(input.withStr);
	const label = extractDisplayName(input.withStr);
	const personId = makePersonId(input.withStr, input.slug);
	const personProps = {};
	if (email !== void 0) personProps["email"] = email;
	if (input.companyName !== void 0) personProps["company"] = input.companyName;
	if (input.domain !== void 0) personProps["domain"] = input.domain;
	const nodes = [{
		id: personId,
		type: "person",
		label,
		properties: personProps
	}];
	if (input.domain !== void 0 || input.companyName !== void 0) {
		const companyId = makeCompanyId(input.domain, input.slug, input.companyName);
		const companyProps = {};
		if (input.domain !== void 0) companyProps["domain"] = input.domain;
		const companyNode = {
			id: companyId,
			type: "company",
			label: input.companyName ?? input.domain ?? input.slug,
			properties: companyProps
		};
		nodes.push(companyNode);
	}
	return nodes;
}
function extractEdges(personId, companyId, interactionDate) {
	if (!companyId) return [];
	return [{
		id: `WORKS_AT:${personId}__${companyId}`,
		from: personId,
		to: companyId,
		type: "WORKS_AT",
		weight: .5,
		sentiment: 0,
		lastContact: interactionDate,
		contactCount: 1,
		properties: {}
	}];
}
async function updateGraphFromInteraction(dataDir, slug, input) {
	if (!input.withStr.trim()) return;
	let domain;
	let companyName;
	const factsPath = path.join(dataDir, "customers", slug, "main_facts.md");
	if (fs.existsSync(factsPath)) try {
		const parsed = matter(fs.readFileSync(factsPath, "utf-8"));
		domain = parsed.data["domain"];
		companyName = parsed.data["name"];
	} catch {}
	const extractionInput = {
		slug,
		withStr: input.withStr,
		interactionDate: input.interactionDate
	};
	if (domain !== void 0) extractionInput.domain = domain;
	if (companyName !== void 0) extractionInput.companyName = companyName;
	const nodes = extractNodes(extractionInput);
	const edges = extractEdges(makePersonId(input.withStr, slug), domain !== void 0 || companyName !== void 0 ? makeCompanyId(domain, slug, companyName) : void 0, input.interactionDate);
	await writeGraph(dataDir, slug, (current) => {
		const empty = {
			schemaVersion: "1",
			slug,
			nodes: [],
			edges: [],
			updatedAt: (/* @__PURE__ */ new Date()).toISOString()
		};
		let graph = current ?? empty;
		for (const node of nodes) graph = upsertNode(graph, node);
		for (const edge of edges) graph = upsertEdge(graph, edge);
		return graph;
	});
}
//#endregion
//#region src/core/relationship-health.ts
function healthPath(dataDir, slug) {
	return path.join(dataDir, "customers", slug, "health.json");
}
function readHealth(dataDir, slug) {
	return readJsonFile(healthPath(dataDir, slug), null);
}
function writeHealth(dataDir, slug, health) {
	writeJsonFile(healthPath(dataDir, slug), {
		...health,
		updatedAt: (/* @__PURE__ */ new Date()).toISOString()
	});
}
function parseContactInteractions(content) {
	const blocks = content.split(/(?=^## \d{4}-\d{2}-\d{2})/m).filter((b) => b.trim().length > 0);
	const result = [];
	for (const block of blocks) {
		const headingMatch = block.match(/^## (\d{4}-\d{2}-\d{2}) · (\w+)/m);
		if (!headingMatch) continue;
		const date = headingMatch[1];
		const type = headingMatch[2];
		const withMatch = block.match(/^\*\*(?:With|Subject):\*\*\s*(.+)$/m);
		if (!withMatch) continue;
		const withStr = withMatch[1].trim();
		result.push({
			date,
			type,
			withStr
		});
	}
	return result;
}
function calcRecencyScore(daysSince) {
	if (daysSince <= 0) return 100;
	if (daysSince >= 30) return 0;
	return Math.round(100 * (1 - daysSince / 30));
}
function calcCadenceScore(daysSince, avgCadenceDays) {
	if (avgCadenceDays <= 0) return 50;
	const ratio = daysSince / avgCadenceDays;
	if (ratio <= 1) return 100;
	if (ratio >= 3) return 0;
	return Math.round(100 * (1 - (ratio - 1) / 2));
}
function calcMomentumScore(last30d, prev30d) {
	if (last30d === 0 && prev30d === 0) return 50;
	if (prev30d === 0) return 80;
	const ratio = last30d / prev30d;
	if (ratio >= 1.5) return 100;
	if (ratio >= 1) return 75;
	if (ratio >= .5) return 50;
	if (ratio >= .25) return 25;
	return 0;
}
function gradeFromScore(score) {
	if (score >= 80) return "A";
	if (score >= 60) return "B";
	if (score >= 40) return "C";
	if (score >= 20) return "D";
	return "F";
}
function trendFromState(score, daysSince, avgCadenceDays, momentumScore) {
	if (score < 20 || daysSince >= 30) return "cold";
	if (momentumScore > 70 && score > 60) return "rising";
	if (momentumScore < 30 || daysSince > avgCadenceDays * 1.5 && score < 60) return "declining";
	return "stable";
}
function calcRiskFlags(_contactId, daysSince, score, isChampion) {
	const flags = [];
	if (daysSince >= 30) flags.push("NO_CONTACT_30D");
	if (daysSince >= 14) flags.push("NO_CONTACT_14D");
	if (isChampion && score < 50) flags.push("CHAMPION_SILENT");
	return flags;
}
function generateRecommendation(name, grade, trend, riskFlags, daysSince, avgCadenceDays) {
	if (riskFlags.includes("NO_CONTACT_30D")) return `Re-engage ${name} urgently — no contact in ${daysSince} days.`;
	if (riskFlags.includes("CHAMPION_SILENT")) return `Champion ${name} has gone quiet — critical to re-engage before deal stalls.`;
	if (riskFlags.includes("NO_CONTACT_14D")) return `Schedule contact with ${name} — ${daysSince} days since last touchpoint.`;
	if (trend === "declining") return `${name} relationship declining — increase touchpoint frequency.`;
	if (grade === "A") return `${name} — strong relationship. Keep current cadence.`;
	const daysUntilDue = Math.max(0, avgCadenceDays - daysSince);
	return `${name} — grade ${grade}. Next contact due in ~${daysUntilDue} day${daysUntilDue === 1 ? "" : "s"}.`;
}
function dateUtcMs(d) {
	return (/* @__PURE__ */ new Date(`${d}T00:00:00Z`)).getTime();
}
function calcAvgCadence(interactions) {
	if (interactions.length < 2) return 0;
	const sorted = [...interactions].sort((a, b) => b.date.localeCompare(a.date));
	let totalDays = 0;
	for (let i = 0; i < sorted.length - 1; i++) {
		const gap = Math.round((dateUtcMs(sorted[i].date) - dateUtcMs(sorted[i + 1].date)) / 864e5);
		totalDays += gap;
	}
	return Math.round(totalDays / (sorted.length - 1));
}
function groupInteractionsByContact(interactions, slug) {
	const map = /* @__PURE__ */ new Map();
	for (const ix of interactions) {
		const email = extractEmail(ix.withStr);
		const name = extractDisplayName(ix.withStr);
		const contactId = makePersonId(ix.withStr, slug);
		if (!map.has(contactId)) {
			const entry = {
				contactId,
				name,
				interactions: []
			};
			if (email !== void 0) entry.email = email;
			map.set(contactId, entry);
		}
		map.get(contactId).interactions.push(ix);
	}
	return Array.from(map.values());
}
function computeContactHealth(group, today, isChampion) {
	const lastContact = [...group.interactions].sort((a, b) => b.date.localeCompare(a.date))[0]?.date ?? "";
	const daysSince = lastContact ? Math.round((dateUtcMs(today) - dateUtcMs(lastContact)) / 864e5) : 999;
	const avgCadenceDays = calcAvgCadence(group.interactions);
	const todayMs = dateUtcMs(today);
	const d30 = todayMs - 30 * 864e5;
	const d60 = todayMs - 60 * 864e5;
	const last30d = group.interactions.filter((i) => dateUtcMs(i.date) >= d30).length;
	const prev30d = group.interactions.filter((i) => dateUtcMs(i.date) >= d60 && dateUtcMs(i.date) < d30).length;
	const recency = calcRecencyScore(daysSince);
	const cadence = calcCadenceScore(daysSince, avgCadenceDays);
	const sentiment = 50;
	const response = 50;
	const momentum = calcMomentumScore(last30d, prev30d);
	const score = Math.round(recency * .35 + cadence * .25 + sentiment * .2 + response * .1 + momentum * .1);
	const grade = gradeFromScore(score);
	const trend = trendFromState(score, daysSince, avgCadenceDays, momentum);
	const riskFlags = calcRiskFlags(group.contactId, daysSince, score, isChampion);
	const recommendation = generateRecommendation(group.name, grade, trend, riskFlags, daysSince, avgCadenceDays);
	const health = {
		contactId: group.contactId,
		name: group.name,
		score,
		grade,
		trend,
		daysSinceContact: daysSince,
		avgCadenceDays,
		sentimentTrend: 0,
		riskFlags,
		lastContact,
		interactionCount30d: last30d,
		recommendation,
		updatedAt: (/* @__PURE__ */ new Date()).toISOString()
	};
	if (group.email !== void 0) health.email = group.email;
	return health;
}
function computeCustomerHealth(dataDir, slug, today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
	const interactionsPath = path.join(dataDir, "customers", slug, "interactions.md");
	const groups = groupInteractionsByContact(parseContactInteractions(fs.existsSync(interactionsPath) ? fs.readFileSync(interactionsPath, "utf-8") : ""), slug);
	const graph = readGraph(dataDir, slug);
	const championIds = new Set(graph.edges.filter((e) => e.type === "IS_CHAMPION").map((e) => e.from));
	const contacts = groups.map((group) => computeContactHealth(group, today, championIds.has(group.contactId)));
	return {
		schemaVersion: "1",
		slug,
		contacts,
		overallHealth: contacts.length === 0 ? 100 : Math.round(contacts.reduce((sum, c) => sum + c.score, 0) / contacts.length),
		updatedAt: (/* @__PURE__ */ new Date()).toISOString()
	};
}
async function updateHealthFromInteraction(dataDir, slug) {
	writeHealth(dataDir, slug, computeCustomerHealth(dataDir, slug));
}
//#endregion
export { updateGraphFromInteraction as a, readGraph as c, writeHealth as i, readHealth as n, findPath as o, updateHealthFromInteraction as r, getStakeholders as s, computeCustomerHealth as t };

//# sourceMappingURL=relationship-health-ZZNXR1RZ.js.map