import os from pathlib import Path from typing import Any from app.schemas.document_verification import ( ContentRiskAnalysis, DeepSeekAnalysis, DocumentVerificationReport, FieldExtractionAnalysis, FileValidationResult, ForensicAnalysis, ImageAnalysis, MetadataAnalysis, PdfAnalysis, QRAnalysis, TextConsistencyAnalysis, TextExtractionAnalysis, VerificationInput, ) from app.services.trust_scorer import TrustScorer LIMITATIONS = [ "BitCheck provides a risk-based estimate, not legal proof of forgery or authenticity.", "Missing metadata does not prove a document is fake.", "Editing software metadata does not automatically prove manipulation.", "OCR may be inaccurate on low-quality scans.", "QR code detection does not mean the linked source is authentic unless externally verified.", "QR URLs are analyzed structurally but are not opened or browsed.", "Forensic visual analysis is not court-grade evidence.", "DeepSeek analysis does not perform live web or issuer database verification.", "High-stakes documents should be manually verified with the issuing authority.", ] class ReportBuilder: def build( self, *, verification_id: str, status: str, processing_time_ms: int, verification_input: VerificationInput, file_validation: FileValidationResult, pdf_analysis: PdfAnalysis | None, image_analysis: ImageAnalysis | None, metadata: MetadataAnalysis, text_extraction: TextExtractionAnalysis, text_consistency: TextConsistencyAnalysis, fields: FieldExtractionAnalysis, content_risk: ContentRiskAnalysis, deepseek_analysis: DeepSeekAnalysis, qr_analysis: QRAnalysis, forensics: ForensicAnalysis | None, warnings: list[str], ) -> DocumentVerificationReport: sanitized_file_validation = self._sanitize_model(file_validation) sanitized_pdf = self._sanitize_model(pdf_analysis) if pdf_analysis else None sanitized_image = self._sanitize_model(image_analysis) if image_analysis else None sanitized_forensics = self._sanitize_model(forensics) if forensics else None trust = TrustScorer().score( { "metadata": metadata, "pdf_analysis": pdf_analysis, "text_extraction": text_extraction, "text_consistency": text_consistency, "qr_analysis": qr_analysis, "forensics": forensics, "fields": fields, "content_risk": content_risk, "deepseek_analysis": deepseek_analysis, } ) risk_flags = self._risk_flags(metadata, pdf_analysis, text_consistency, qr_analysis, forensics, fields, content_risk, trust) all_warnings = self._dedupe(warnings) return DocumentVerificationReport( verification_id=verification_id, service="BitCheck", file_type="document", status=status, processing_time_ms=processing_time_ms, input=verification_input, file_validation=sanitized_file_validation, pdf_analysis=sanitized_pdf, image_analysis=sanitized_image, metadata=metadata, text_extraction=self._redact_text_extraction(text_extraction), text_consistency=text_consistency, fields=fields, content_risk=content_risk, deepseek_analysis=deepseek_analysis, qr_analysis=qr_analysis, forensics=sanitized_forensics, trust=trust, risk_flags=risk_flags, recommended_actions=self._recommended_actions(trust, risk_flags, deepseek_analysis), limitations=LIMITATIONS, warnings=all_warnings, ) def _risk_flags( self, metadata: MetadataAnalysis, pdf_analysis: PdfAnalysis | None, text_consistency: TextConsistencyAnalysis, qr_analysis: QRAnalysis, forensics: ForensicAnalysis | None, fields: FieldExtractionAnalysis, content_risk: ContentRiskAnalysis, trust, ) -> list[str]: flags: list[str] = [] flags.extend(metadata.flags) if pdf_analysis: flags.extend(pdf_analysis.flags) flags.extend(text_consistency.flags) flags.extend(qr_analysis.flags) if forensics: flags.extend(forensics.flags) flags.extend(fields.field_flags) flags.extend(content_risk.signals) flags.extend(trust.applied_overrides) return self._dedupe(flags) def _recommended_actions(self, trust, risk_flags: list[str], deepseek_analysis: DeepSeekAnalysis) -> list[str]: actions: list[str] = [] if trust.decision == "approve": actions.append("Proceed only after routine human review of the document details.") elif trust.risk_level == "Suspicious": actions.append("Manually review the document before accepting it.") else: actions.append("Do not approve automatically; escalate for manual verification.") if deepseek_analysis.external_verification_required: actions.append("Verify the document directly with the issuing authority or official portal.") if any("QR" in flag or flag.startswith("shortened_url") or flag.startswith("suspicious_url") for flag in risk_flags): actions.append("Check QR destination ownership and compare it with official issuer channels.") if any("metadata" in flag.lower() for flag in risk_flags): actions.append("Review metadata in context rather than treating it as standalone proof.") if any("visual" in flag.lower() or "tampering" in flag.lower() for flag in risk_flags): actions.append("Inspect highlighted visual regions manually.") return self._dedupe(actions) def _redact_text_extraction(self, text_extraction: TextExtractionAnalysis) -> TextExtractionAnalysis: return text_extraction.model_copy( update={ "page_texts": [self._truncate(text, 240) for text in text_extraction.page_texts], "combined_text_excerpt": self._truncate(text_extraction.combined_text_excerpt, 500), } ) def _sanitize_model(self, model): data = model.model_dump() sanitized = self._sanitize_paths(data) return model.__class__.model_validate(sanitized) def _sanitize_paths(self, value: Any) -> Any: if isinstance(value, dict): return {key: self._sanitize_paths(item) for key, item in value.items()} if isinstance(value, list): return [self._sanitize_paths(item) for item in value] if isinstance(value, str): return self._relative_if_path(value) return value def _relative_if_path(self, value: str) -> str: try: path = Path(value) except (TypeError, ValueError): return value if not path.is_absolute(): return value try: return os.path.relpath(path, Path.cwd()) except ValueError: return value def _truncate(self, text: str, max_chars: int) -> str: if len(text) <= max_chars: return text return text[: max_chars - 3].rstrip() + "..." def _dedupe(self, values: list[str]) -> list[str]: return [value for value in dict.fromkeys(values) if value]