import type { CreateNeedInput } from '../../../domain/need' import type { ParsedNeedCriteria, FollowUpQuestion } from '../../../domain/needBuilder' import type { UnifiedMatchResult } from '../../../domain/unifiedResult' import type { IAIService, AIResponse, DecisionBrief, ComparisonSummary, CriteriaExtractionResult, OfferEmailPayload, MatchExplanationInput, MatchExplanation, TradeOffInput, TradeOffSummary, DataQualityInput, DataQualitySummary, MarketSignalClassification, } from '../IAIService' import { mockProvenance } from '../IAIService' import { mockParseNeed } from './needParser' import { buildComparisonSummary } from './compareBuilder' import { buildMockDecisionBrief } from './decisionBrief' const SIMULATED_DELAY = { fast: 300, medium: 600, slow: 1800 } const delay = (ms: number) => new Promise(r => setTimeout(r, ms)) // ── Follow-up question templates keyed by ParsedNeedCriteria field ──────────── interface QuestionTemplate { questionText: string reason: string suggestedAnswerOptions?: string[] importance: FollowUpQuestion['importance'] } const FOLLOW_UP_TEMPLATES: Partial> = { assetType: { questionText: 'Welchen Nutzungstyp suchen Sie?', reason: 'Nutzungstyp ist zwingend für die Matchsuche', suggestedAnswerOptions: ['Büro', 'Retail', 'Logistik', 'Produktion', 'Gastro'], importance: 'required', }, areaRange: { questionText: 'Welche Fläche benötigen Sie (min–max in m²)?', reason: 'Flächenbedarf ist zwingend für die Filterung', importance: 'required', }, preferredLocations: { questionText: 'In welchen Städten oder Regionen suchen Sie?', reason: 'Standortpräferenz fehlt', suggestedAnswerOptions: ['Zürich', 'Basel', 'Bern', 'Zug', 'Genf', 'Lausanne'], importance: 'required', }, budgetRange: { questionText: 'Was ist Ihr maximales Budget pro m² und Monat (CHF)?', reason: 'Budget ist wichtig für die Filterung unpassender Objekte', importance: 'recommended', }, timing: { questionText: 'Wann möchten Sie spätestens einziehen?', reason: 'Verfügbarkeitstermin fehlt', importance: 'recommended', }, mustHaveCriteria: { questionText: 'Haben Sie zwingende Anforderungen (ÖV-Anbindung, Parkplätze, Laderampe)?', reason: 'Pflichtkriterien sind für die Filterung relevant', importance: 'optional', }, } function buildFollowUpQuestions(criteria: ParsedNeedCriteria): FollowUpQuestion[] { const missing: Array = [] if (!criteria.assetType) missing.push('assetType') if (!criteria.areaRange) missing.push('areaRange') if (!criteria.preferredLocations?.length) missing.push('preferredLocations') if (!criteria.budgetRange) missing.push('budgetRange') if (!criteria.timing) missing.push('timing') if (!criteria.mustHaveCriteria?.length) missing.push('mustHaveCriteria') return missing .slice(0, 3) .map((field, i) => { const tpl = FOLLOW_UP_TEMPLATES[field] if (!tpl) return null const q: FollowUpQuestion = { id: `fq-mock-${i}`, questionText: tpl.questionText, targetField: field, reason: tpl.reason, importance: tpl.importance, suggestedAnswerOptions: tpl.suggestedAnswerOptions, } return q }) .filter((q): q is FollowUpQuestion => q !== null) } // ── Service ─────────────────────────────────────────────────────────────────── export const MockAIService: IAIService = { async parseNeed(input: string): Promise>> { await delay(SIMULATED_DELAY.fast) return { data: mockParseNeed(input), provenance: mockProvenance() } }, async generateFollowUpQuestions(criteria: ParsedNeedCriteria): Promise> { await delay(SIMULATED_DELAY.medium) return { data: buildFollowUpQuestions(criteria), provenance: mockProvenance() } }, async generateMatchExplanation(input: MatchExplanationInput): Promise> { await delay(SIMULATED_DELAY.medium) const isStrong = input.matchScore >= 78 const isMedium = input.matchScore >= 52 const headline = isStrong ? `Starkes Match — ${input.propertyTitle} erfüllt Ihre Kernkriterien hervorragend` : isMedium ? `Gutes Match mit einzelnen Kompromissen für ${input.propertyTitle}` : `Schwaches Match — mehrere Kriterien nicht erfüllt bei ${input.propertyTitle}` const positiveText = input.positiveFactors.slice(0, 2).map(f => f.explanation).join('; ') const negativeText = input.negativeFactors.slice(0, 1).map(f => f.explanation).join('; ') const summary = `${input.propertyTitle} in ${input.propertyCity} erreicht ${input.matchScore}/100 Punkte.${positiveText ? ` Hauptstärken: ${positiveText}.` : ''}${negativeText ? ` Einschränkung: ${negativeText}.` : ''}` return { data: { headline, summary, keyReasons: [ ...input.positiveFactors.slice(0, 2).map(f => `+ ${f.explanation}`), ...input.negativeFactors.slice(0, 1).map(f => `− ${f.explanation}`), ], }, provenance: mockProvenance(), } }, async summarizeTradeOffs(tradeoffs: TradeOffInput[]): Promise> { await delay(SIMULATED_DELAY.fast) const critical = tradeoffs.filter(t => t.severity === 'HIGH') const overallRisk: TradeOffSummary['overallRisk'] = critical.length >= 2 ? 'HIGH' : critical.length === 1 ? 'MEDIUM' : 'LOW' const riskLabel = overallRisk === 'HIGH' ? 'Hoch' : overallRisk === 'MEDIUM' ? 'Mittel' : 'Gering' return { data: { headline: tradeoffs.length === 0 ? 'Keine wesentlichen Trade-offs identifiziert' : `${tradeoffs.length} Trade-off${tradeoffs.length > 1 ? 's' : ''} — Gesamtrisiko: ${riskLabel}`, items: tradeoffs.map(t => ({ concern: t.concern, severity: t.severity, mitigation: t.mitigation })), overallRisk, }, provenance: mockProvenance(), } }, async summarizeComparison(items: UnifiedMatchResult[]): Promise> { await delay(SIMULATED_DELAY.medium) return { data: buildComparisonSummary(items), provenance: mockProvenance() } }, async generateDecisionBrief(shortlistId: string): Promise> { await delay(SIMULATED_DELAY.slow) return { data: buildMockDecisionBrief(shortlistId), provenance: mockProvenance() } }, async generateDataQualitySummary(_propertyId: string, quality: DataQualityInput): Promise> { await delay(SIMULATED_DELAY.fast) const level = quality.score >= 0.85 ? 'excellent' : quality.score >= 0.70 ? 'good' : quality.score >= 0.55 ? 'fair' : quality.score >= 0.40 ? 'poor' : 'critical' const assessments: Record = { excellent: 'Exzellente Datenqualität — alle Kernfelder vollständig und aktuell.', good: 'Gute Datenqualität — kleinere Lücken beeinflussen die Matchgenauigkeit nicht wesentlich.', fair: 'Ausreichende Datenqualität — fehlende Felder können die Matchgenauigkeit beeinträchtigen.', poor: 'Geringe Datenqualität — wichtige Felder fehlen, Match-Score mit Vorsicht interpretieren.', critical: 'Kritische Datenqualität — fundamentale Felder fehlen, Match-Ergebnis stark eingeschränkt.', } const hasCritical = quality.missingCriticalFields.length > 0 return { data: { overallAssessment: assessments[level], missingCriticalFields: quality.missingCriticalFields, recommendation: hasCritical ? `Fehlende Pflichtfelder ergänzen: ${quality.missingCriticalFields.join(', ')}` : quality.score < 0.70 ? 'Daten aktualisieren und optionale Felder ergänzen für bessere Matchgenauigkeit.' : 'Keine sofortigen Massnahmen erforderlich.', confidence: quality.score, }, provenance: mockProvenance(), } }, async classifyMarketSignal(signalText: string): Promise> { await delay(SIMULATED_DELAY.medium) const t = signalText.toLowerCase() let signalType: MarketSignalClassification['signalType'] = 'UNKNOWN' if (t.includes('neubau') || t.includes('baubewilligung') || t.includes('umbau')) signalType = 'CONSTRUCTION' else if (t.includes('expansion') || t.includes('wachstum') || t.includes('sucht fläche')) signalType = 'EXPANSION' else if (t.includes('verlegt') || t.includes('umzug') || t.includes('relocation')) signalType = 'RELOCATION' else if (t.includes('stellenabbau') || t.includes('restruktur') || t.includes('fusion')) signalType = 'RESTRUCTURING' else if (t.includes('frei') || t.includes('kündigung') || t.includes('schliessung') || t.includes('leerstand')) signalType = 'VACANCY' const areaMatch = signalText.match(/(\d{2,5})\s*m²/) const monthsMatch = signalText.match(/(\d{1,2})\s*Monate?n?/) return { data: { signalType, probability: 0.65, timeHorizonMonths: monthsMatch ? parseInt(monthsMatch[1]) : null, areaSqmEstimate: areaMatch ? parseInt(areaMatch[1]) : null, credibility: 'MEDIUM', reasoning: `Keyword-basierte Klassifikation (Mock). Signaltyp: ${signalType}.`, }, provenance: mockProvenance(), } }, async generateOfferEmail(payload: OfferEmailPayload): Promise> { await delay(SIMULATED_DELAY.medium * 2) return { data: { subject: `Passende Gewerbeflächen zu Ihrer Anfrage: ${payload.needTitle}`, body: `Sehr geehrte Damen und Herren,\n\nvielen Dank für Ihr Interesse. Gerne unterbreiten wir Ihnen folgende passende Gewerbeobjekte aus unserem Portfolio:\n\n` + payload.properties.map((p, i) => `• ${p} (Match-Score: ${payload.matchScores[i]}%)`).join('\n') + `\n\nGerne arrangieren wir Besichtigungstermine für die genannten Objekte und stehen für alle weiteren Fragen zur Verfügung.\n\nFreundliche Grüsse\nWincasa AG`, }, provenance: mockProvenance(), } }, // Legacy methods async extractCriteria(_input: string): Promise> { return { data: { extractedCriteria: { companyName: 'Unbekannt (bitte bestätigen)', requiredArea: { min: 400, max: 900 }, budgetRange: { maxPerSqm: 40, currency: 'CHF' }, }, confidence: 0.72, missingFields: ['assetType', 'timing', 'preferredLocations'], assumptions: ['Fläche aus Zahlenangabe geschätzt', 'Budget aus Kostennennung abgeleitet'], followUpQuestions: [ 'Welchen Nutzungstyp suchen Sie (Büro, Retail, Logistik)?', 'In welchen Städten oder Regionen suchen Sie?', 'Wann möchten Sie spätestens einziehen?', ], }, provenance: mockProvenance(), } }, async generateFollowUp(partialNeed: Partial): Promise> { const questions: string[] = [] if (!partialNeed.assetType) questions.push('Welchen Nutzungstyp suchen Sie?') if (!partialNeed.preferredLocations?.length) questions.push('In welchen Regionen suchen Sie?') if (!partialNeed.timing) questions.push('Was ist Ihr gewünschter Einzugstermin?') if (!partialNeed.budgetRange) questions.push('Was ist Ihr maximales monatliches Budget?') return { data: questions, provenance: mockProvenance() } }, }