/**
 * Knowledge Base Service
 * Handles document management, FAQ management, and RAG queries
 * Integrates with Gemini File Search API for RAG
 *
 * All data operations use Convex directly via convexQuery/convexMutation.
 */

import { convexQuery, convexMutation } from '@/lib/convex-client';
import { logger } from '@/lib/logger';
import { cache } from '@/lib/cache';
import { createId } from '@paralleldrive/cuid2';
import { GeminiRAGService } from './gemini-rag.service';
import type {
  KnowledgeBaseDocument,
  KnowledgeBaseFAQ,
  KnowledgeBaseSettings,
  KBDocumentStatus,
  CreateFAQRequest,
  UpdateFAQRequest,
  RAGQueryResponse,
} from '@/lib/types/knowledge-base.types';

// Alias: Gemini fields are part of KnowledgeBaseDocument via Convex
type KnowledgeBaseDocumentWithGemini = KnowledgeBaseDocument & {
  geminiFileName?: string | null;
  geminiUploadedAt?: Date | null;
  deletedAt?: Date | null;
};

const scopedLogger = logger.scope('KnowledgeBaseService');
const KB_STATS_CACHE_TTL_SECONDS = 600; // 10 minutes
const KB_FAQ_CACHE_TTL_SECONDS = 900; // 15 minutes

const statsCacheKey = (userId: string) => `kb:stats:v1:${userId}`;
const faqCacheKey = (userId: string) => `kb:faqs:v1:${userId}:active-default`;

async function invalidateKnowledgeBaseCaches(userId: string): Promise<void> {
  await Promise.allSettled([
    cache.del(statsCacheKey(userId)),
    cache.del(faqCacheKey(userId)),
  ]);
}

/**
 * Convert a Convex document (with _id / _creationTime) to the app-level type.
 * Convex returns _id as the internal ID and timestamps as epoch ms.
 */
function convexToDoc<T>(doc: Record<string, unknown> | null): T | null {
  if (!doc) return null;
  const { _id, _creationTime, ...rest } = doc;
  return {
    id: (doc.prismaId as string) || (_id as string),
    ...rest,
    createdAt: rest.createdAt ? new Date(rest.createdAt as number) : new Date(_creationTime as number),
    updatedAt: rest.updatedAt ? new Date(rest.updatedAt as number) : new Date(),
    ...(rest.processedAt ? { processedAt: new Date(rest.processedAt as number) } : {}),
    ...(rest.deletedAt ? { deletedAt: new Date(rest.deletedAt as number) } : {}),
    ...(rest.geminiUploadedAt ? { geminiUploadedAt: new Date(rest.geminiUploadedAt as number) } : {}),
  } as unknown as T;
}

/**
 * Strip undefined values from an object so Convex doesn't reject them.
 */
function stripUndefined(obj: Record<string, unknown>): Record<string, unknown> {
  const result: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(obj)) {
    if (value !== undefined) {
      result[key] = value;
    }
  }
  return result;
}

export class KnowledgeBaseService {
  /**
   * Get or create knowledge base settings for a user
   */
  static async getOrCreateSettings(userId: string): Promise<KnowledgeBaseSettings> {
    let raw = await convexQuery<Record<string, unknown>>(
      'voice_agents/queries:getKBSettingsByUserId',
      { userId },
    );

    if (!raw) {
      const prismaId = createId();
      await convexMutation('voice_agents/mutations:createKBSettings', {
        userId,
        prismaId,
        isEnabled: true,
        onboardingCompleted: false,
        maxChunksPerQuery: 5,
        similarityThreshold: 0.7,
      });
      scopedLogger.info('Created knowledge base settings', { userId });

      // Re-query to get the full document
      raw = await convexQuery<Record<string, unknown>>(
        'voice_agents/queries:getKBSettingsByUserId',
        { userId },
      );
    }

    return convexToDoc<KnowledgeBaseSettings>(raw)!;
  }

  /**
   * Update knowledge base settings
   */
  static async updateSettings(
    userId: string,
    data: Partial<Omit<KnowledgeBaseSettings, 'id' | 'userId' | 'createdAt' | 'updatedAt'>>
  ): Promise<KnowledgeBaseSettings> {
    // Ensure settings exist first (upsert pattern)
    const existing = await this.getOrCreateSettings(userId);

    await convexMutation(
      'voice_agents/mutations:updateKBSettings',
      stripUndefined({
        id: existing.id,
        ...data,
        updatedAt: Date.now(),
      }),
    );

    scopedLogger.info('Updated knowledge base settings', { userId });

    // Re-query to return the updated record
    const raw = await convexQuery<Record<string, unknown>>(
      'voice_agents/queries:getKBSettingsByUserId',
      { userId },
    );
    return convexToDoc<KnowledgeBaseSettings>(raw)!;
  }

  /**
   * Mark onboarding as completed
   */
  static async completeOnboarding(userId: string): Promise<KnowledgeBaseSettings> {
    return this.updateSettings(userId, { onboardingCompleted: true });
  }

  // ============ Document Management ============

  /**
   * Create a new document record (before upload)
   */
  static async createDocument(
    userId: string,
    data: {
      filename: string;
      originalName: string;
      mimeType: string;
      fileSize: number;
      storagePath: string;
      title?: string;
      description?: string;
      category?: string;
    }
  ): Promise<KnowledgeBaseDocument> {
    const prismaId = createId();
    const convexId = await convexMutation<string>(
      'voice_agents/mutations:createKBDocument',
      stripUndefined({
        userId,
        prismaId,
        filename: data.filename,
        originalName: data.originalName,
        mimeType: data.mimeType,
        fileSize: data.fileSize,
        storagePath: data.storagePath,
        title: data.title,
        description: data.description,
        category: data.category || 'general',
      }),
    );

    // Fetch the created document by its Convex ID
    const created = await convexQuery<Record<string, unknown>>(
      'voice_agents/queries:getKBDocumentById',
      { id: convexId || prismaId },
    );

    scopedLogger.info('Document created', { userId, documentId: prismaId });
    await invalidateKnowledgeBaseCaches(userId);

    return convexToDoc<KnowledgeBaseDocument>(created)!;
  }

  /**
   * Get all documents for a user
   */
  static async getDocuments(
    userId: string,
    options?: {
      category?: string;
      status?: KBDocumentStatus;
      limit?: number;
      offset?: number;
    }
  ): Promise<{ documents: KnowledgeBaseDocument[]; total: number }> {
    const raw = await convexQuery<Record<string, unknown>[]>(
      'voice_agents/queries:getKBDocuments',
      stripUndefined({
        userId,
        category: options?.category,
        status: options?.status,
      }),
    );

    const allDocs = (raw || [])
      // Exclude soft-deleted documents
      .filter((d) => !d.deletedAt)
      .map((d) => convexToDoc<KnowledgeBaseDocument>(d)!);

    // Sort by createdAt descending
    allDocs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());

    // Client-side pagination
    const offset = options?.offset || 0;
    const limit = options?.limit || 50;
    const total = allDocs.length;
    const documents = allDocs.slice(offset, offset + limit);

    return { documents, total };
  }

  /**
   * Get a single document by ID
   */
  static async getDocument(
    userId: string,
    documentId: string
  ): Promise<KnowledgeBaseDocument | null> {
    const raw = await convexQuery<Record<string, unknown>>(
      'voice_agents/queries:getKBDocumentById',
      { id: documentId },
    );

    if (!raw) return null;

    // Verify the document belongs to the user
    if (raw.userId !== userId && raw.prismaId !== documentId) {
      // Also check prismaId match in case documentId is the prismaId
      const rawUserId = raw.userId as string;
      if (rawUserId !== userId) return null;
    }

    return convexToDoc<KnowledgeBaseDocument>(raw);
  }

  /**
   * Update document metadata
   */
  static async updateDocument(
    userId: string,
    documentId: string,
    data: {
      title?: string;
      description?: string;
      category?: string;
    }
  ): Promise<KnowledgeBaseDocument | null> {
    // Verify ownership first
    const existing = await this.getDocument(userId, documentId);
    if (!existing) return null;

    await convexMutation(
      'voice_agents/mutations:updateKBDocument',
      stripUndefined({
        id: documentId,
        ...data,
        updatedAt: Date.now(),
      }),
    );

    await invalidateKnowledgeBaseCaches(userId);

    return this.getDocument(userId, documentId);
  }

  /**
   * Update document processing status
   */
  static async updateDocumentStatus(
    documentId: string,
    status: KBDocumentStatus,
    options?: {
      processingError?: string;
      chunkCount?: number;
      embeddingModel?: string;
    }
  ): Promise<void> {
    await convexMutation(
      'voice_agents/mutations:updateKBDocument',
      stripUndefined({
        id: documentId,
        status,
        processedAt: status === 'COMPLETED' ? Date.now() : undefined,
        processingError: options?.processingError,
        chunkCount: options?.chunkCount || 0,
        embeddingModel: options?.embeddingModel,
      }),
    );

    scopedLogger.info('Document status updated', { documentId, status });
  }

  /**
   * Delete a document and its chunks
   */
  static async deleteDocument(
    userId: string,
    documentId: string
  ): Promise<boolean> {
    // Verify ownership
    const existing = await this.getDocument(userId, documentId);
    if (!existing) return false;

    await convexMutation(
      'voice_agents/mutations:deleteKBDocument',
      { id: documentId },
    );

    scopedLogger.info('Document deleted', { userId, documentId });
    await invalidateKnowledgeBaseCaches(userId);

    return true;
  }

  // ============ FAQ Management ============

  /**
   * Create a new FAQ
   */
  static async createFAQ(
    userId: string,
    data: CreateFAQRequest
  ): Promise<KnowledgeBaseFAQ> {
    const prismaId = createId();
    const convexId = await convexMutation<string>(
      'voice_agents/mutations:createKBFAQ',
      stripUndefined({
        userId,
        prismaId,
        questionEn: data.questionEn,
        questionAr: data.questionAr,
        answerEn: data.answerEn,
        answerAr: data.answerAr,
        category: data.category || 'general',
        priority: data.priority || 0,
        isActive: true,
      }),
    );

    // Fetch the created FAQ by querying all and finding by ID
    const faqs = await convexQuery<Record<string, unknown>[]>(
      'voice_agents/queries:getKBFAQs',
      { userId },
    );
    const created = (faqs || []).find(
      (f) => (f._id as string) === convexId || (f.prismaId as string) === prismaId,
    );

    scopedLogger.info('FAQ created', { userId, faqId: prismaId });
    await invalidateKnowledgeBaseCaches(userId);

    return convexToDoc<KnowledgeBaseFAQ>(created || null)!;
  }

  /**
   * Get all FAQs for a user
   */
  static async getFAQs(
    userId: string,
    options?: {
      category?: string;
      activeOnly?: boolean;
      limit?: number;
      offset?: number;
    }
  ): Promise<{ faqs: KnowledgeBaseFAQ[]; total: number }> {
    const cacheable =
      !options?.category &&
      options?.activeOnly !== false &&
      (options?.offset ?? 0) === 0 &&
      options?.limit === undefined;

    const loadFaqs = async () => {
      const isActive = options?.activeOnly !== false ? true : undefined;

      const raw = await convexQuery<Record<string, unknown>[]>(
        'voice_agents/queries:getKBFAQs',
        stripUndefined({
          userId,
          category: options?.category,
          isActive,
        }),
      );

      const allFaqs = (raw || []).map((f) => convexToDoc<KnowledgeBaseFAQ>(f)!);

      // Sort by priority desc, then createdAt desc
      allFaqs.sort((a, b) => {
        if (b.priority !== a.priority) return b.priority - a.priority;
        return b.createdAt.getTime() - a.createdAt.getTime();
      });

      // Client-side pagination
      const offset = options?.offset || 0;
      const limit = options?.limit || 100;
      const total = allFaqs.length;
      const faqs = allFaqs.slice(offset, offset + limit);

      return { faqs, total };
    };

    if (cacheable) {
      return cache.getOrSet(
        faqCacheKey(userId),
        loadFaqs,
        KB_FAQ_CACHE_TTL_SECONDS
      );
    }

    return loadFaqs();
  }

  /**
   * Get a single FAQ by ID
   */
  static async getFAQ(
    userId: string,
    faqId: string
  ): Promise<KnowledgeBaseFAQ | null> {
    // Query all FAQs for this user and find by id
    const raw = await convexQuery<Record<string, unknown>[]>(
      'voice_agents/queries:getKBFAQs',
      { userId },
    );

    const match = (raw || []).find(
      (f) => (f.prismaId as string) === faqId || (f._id as string) === faqId,
    );

    if (!match) return null;
    return convexToDoc<KnowledgeBaseFAQ>(match);
  }

  /**
   * Update a FAQ
   */
  static async updateFAQ(
    userId: string,
    faqId: string,
    data: UpdateFAQRequest
  ): Promise<KnowledgeBaseFAQ | null> {
    // Verify ownership
    const existing = await this.getFAQ(userId, faqId);
    if (!existing) return null;

    await convexMutation(
      'voice_agents/mutations:updateKBFAQ',
      stripUndefined({
        id: faqId,
        ...data,
        updatedAt: Date.now(),
      }),
    );

    await invalidateKnowledgeBaseCaches(userId);

    return this.getFAQ(userId, faqId);
  }

  /**
   * Delete a FAQ
   */
  static async deleteFAQ(userId: string, faqId: string): Promise<boolean> {
    // Verify ownership
    const existing = await this.getFAQ(userId, faqId);
    if (!existing) return false;

    await convexMutation(
      'voice_agents/mutations:deleteKBFAQ',
      { id: faqId },
    );

    scopedLogger.info('FAQ deleted', { userId, faqId });
    await invalidateKnowledgeBaseCaches(userId);

    return true;
  }

  // ============ RAG / Query ============

  /**
   * Store document chunks with embeddings
   */
  static async storeChunks(
    documentId: string,
    chunks: Array<{
      content: string;
      chunkIndex: number;
      embedding?: number[];
      metadata?: Record<string, unknown>;
    }>
  ): Promise<void> {
    for (const chunk of chunks) {
      await convexMutation(
        'voice_agents/mutations:createKBChunk',
        stripUndefined({
          documentId,
          content: chunk.content,
          chunkIndex: chunk.chunkIndex,
          embedding: chunk.embedding,
          metadata: chunk.metadata,
        }),
      );
    }

    scopedLogger.info('Stored chunks', { documentId, count: chunks.length });
  }

  /**
   * Get knowledge base statistics for a user
   */
  static async getStats(userId: string): Promise<{
    totalDocuments: number;
    processedDocuments: number;
    pendingDocuments: number;
    failedDocuments: number;
    totalFAQs: number;
    activeFAQs: number;
    totalChunks: number;
  }> {
    const loadStats = async () => {
      const [rawDocs, rawFaqs] = await Promise.all([
        convexQuery<Record<string, unknown>[]>(
          'voice_agents/queries:getKBDocuments',
          { userId },
        ),
        convexQuery<Record<string, unknown>[]>(
          'voice_agents/queries:getKBFAQs',
          { userId },
        ),
      ]);

      const docs = rawDocs || [];
      const faqs = rawFaqs || [];

      const totalDocuments = docs.length;
      const processedDocuments = docs.filter((d) => d.status === 'COMPLETED').length;
      const pendingDocuments = docs.filter((d) => d.status === 'PENDING').length;
      const failedDocuments = docs.filter((d) => d.status === 'FAILED').length;
      const totalFAQs = faqs.length;
      const activeFAQs = faqs.filter((f) => f.isActive === true).length;

      const totalChunks = docs
        .filter((d) => d.status === 'COMPLETED')
        .reduce((sum: number, d) => sum + ((d.chunkCount as number) || 0), 0);

      return {
        totalDocuments,
        processedDocuments,
        pendingDocuments,
        failedDocuments,
        totalFAQs,
        activeFAQs,
        totalChunks,
      };
    };

    return cache.getOrSet(
      statsCacheKey(userId),
      loadStats,
      KB_STATS_CACHE_TTL_SECONDS
    );
  }

  /**
   * Simple text-based search (placeholder for vector search)
   * In production, this would use vector embeddings for semantic search
   */
  static async searchKnowledgeBase(
    userId: string,
    query: string,
    options?: {
      maxResults?: number;
      includeChunks?: boolean;
      includeFAQs?: boolean;
    }
  ): Promise<{
    chunks: Array<{
      id: string;
      documentId: string;
      documentTitle: string;
      content: string;
      relevanceScore: number;
    }>;
    faqs: KnowledgeBaseFAQ[];
  }> {
    const maxResults = options?.maxResults || 5;
    const results: {
      chunks: Array<{
        id: string;
        documentId: string;
        documentTitle: string;
        content: string;
        relevanceScore: number;
      }>;
      faqs: KnowledgeBaseFAQ[];
    } = { chunks: [], faqs: [] };

    const queryLower = query.toLowerCase();

    // Simple keyword search for chunks
    if (options?.includeChunks !== false) {
      // Get all completed documents for this user
      const rawDocs = await convexQuery<Record<string, unknown>[]>(
        'voice_agents/queries:getKBDocuments',
        { userId, status: 'COMPLETED' },
      );

      const completedDocs = rawDocs || [];

      // For each completed document, get its chunks and filter by keyword
      const chunkResults: Array<{
        id: string;
        documentId: string;
        documentTitle: string;
        content: string;
        relevanceScore: number;
      }> = [];

      for (const doc of completedDocs) {
        const docId = (doc.prismaId as string) || (doc._id as string);
        const rawChunks = await convexQuery<Record<string, unknown>[]>(
          'voice_agents/queries:getKBChunksByDocumentId',
          { documentId: docId },
        );

        for (const chunk of rawChunks || []) {
          const content = chunk.content as string;
          if (content.toLowerCase().includes(queryLower)) {
            chunkResults.push({
              id: (chunk.prismaId as string) || (chunk._id as string),
              documentId: docId,
              documentTitle: (doc.title as string) || (doc.originalName as string),
              content,
              relevanceScore: 1 - chunkResults.length * 0.1,
            });
          }
          if (chunkResults.length >= maxResults) break;
        }
        if (chunkResults.length >= maxResults) break;
      }

      results.chunks = chunkResults.slice(0, maxResults);
    }

    // Search FAQs by keyword
    if (options?.includeFAQs !== false) {
      const rawFaqs = await convexQuery<Record<string, unknown>[]>(
        'voice_agents/queries:getKBFAQs',
        { userId, isActive: true },
      );

      const matchingFaqs = (rawFaqs || [])
        .filter((f) => {
          const qEn = ((f.questionEn as string) || '').toLowerCase();
          const qAr = ((f.questionAr as string) || '').toLowerCase();
          const aEn = ((f.answerEn as string) || '').toLowerCase();
          const aAr = ((f.answerAr as string) || '').toLowerCase();
          return (
            qEn.includes(queryLower) ||
            qAr.includes(queryLower) ||
            aEn.includes(queryLower) ||
            aAr.includes(queryLower)
          );
        })
        .sort((a, b) => ((b.priority as number) || 0) - ((a.priority as number) || 0))
        .slice(0, maxResults)
        .map((f) => convexToDoc<KnowledgeBaseFAQ>(f)!);

      results.faqs = matchingFaqs;
    }

    return results;
  }

  // ============ Gemini RAG Integration ============

  /**
   * Upload a document to Gemini FileSearch and create DB record
   */
  static async uploadDocumentWithGemini(
    userId: string,
    file: {
      buffer: Buffer;
      originalName: string;
      mimeType: string;
    },
    metadata?: {
      title?: string;
      description?: string;
      category?: string;
    }
  ): Promise<KnowledgeBaseDocumentWithGemini> {
    // Create document record first
    const prismaId = createId();
    const filename = `${Date.now()}-${file.originalName}`;

    await convexMutation(
      'voice_agents/mutations:createKBDocument',
      stripUndefined({
        userId,
        prismaId,
        filename,
        originalName: file.originalName,
        mimeType: file.mimeType,
        fileSize: file.buffer.length,
        storagePath: 'gemini', // Documents are stored in Gemini, not locally
        title: metadata?.title || null,
        description: metadata?.description || null,
        category: metadata?.category || 'general',
        status: 'PROCESSING',
      }),
    );

    // Find the created document ID
    const docs = await convexQuery<Record<string, unknown>[]>(
      'voice_agents/queries:getKBDocuments',
      { userId },
    );
    const createdDoc = (docs || []).find((d) => (d.prismaId as string) === prismaId);
    const documentId = createdDoc
      ? (createdDoc.prismaId as string) || (createdDoc._id as string)
      : prismaId;

    try {
      // Upload to Gemini FileSearch
      const geminiResult = await GeminiRAGService.uploadDocument(userId, file, {
        title: metadata?.title,
        category: metadata?.category,
      });

      // Update document with Gemini reference
      await convexMutation(
        'voice_agents/mutations:updateKBDocument',
        stripUndefined({
          id: documentId,
          geminiFileName: geminiResult.geminiFileName,
          geminiUploadedAt: Date.now(),
          status: 'COMPLETED',
          processedAt: Date.now(),
        }),
      );

      scopedLogger.info('Document uploaded to Gemini', {
        userId,
        documentId,
        geminiFileName: geminiResult.geminiFileName,
      });
      await invalidateKnowledgeBaseCaches(userId);

      // Re-fetch the updated document
      const updatedRaw = await convexQuery<Record<string, unknown>>(
        'voice_agents/queries:getKBDocumentById',
        { id: documentId },
      );

      return convexToDoc<KnowledgeBaseDocumentWithGemini>(updatedRaw)!;
    } catch (error) {
      // Mark as failed if Gemini upload fails
      await convexMutation(
        'voice_agents/mutations:updateKBDocument',
        stripUndefined({
          id: documentId,
          status: 'FAILED',
          processingError: error instanceof Error ? error.message : 'Upload failed',
        }),
      );

      const errorMessage = error instanceof Error ? error.message : 'Unknown error';
      scopedLogger.error('Failed to upload document to Gemini', { error: errorMessage }, {
        userId,
        documentId,
      });

      await invalidateKnowledgeBaseCaches(userId);
      throw error;
    }
  }

  /**
   * Query knowledge base using Gemini RAG
   */
  static async queryWithGeminiRAG(
    userId: string,
    query: string,
    options?: {
      language?: 'en' | 'ar';
      maxResults?: number;
      includeCitations?: boolean;
      customInstructions?: string;
    }
  ): Promise<RAGQueryResponse> {
    return GeminiRAGService.queryWithRAG(userId, query, options);
  }

  /**
   * Soft delete a document (marks for deletion, Gemini cleanup happens later)
   */
  static async softDeleteDocument(
    userId: string,
    documentId: string
  ): Promise<boolean> {
    // Verify ownership and that it's not already soft-deleted
    const existing = await this.getDocument(userId, documentId);
    if (!existing) return false;

    await convexMutation(
      'voice_agents/mutations:updateKBDocument',
      { id: documentId, deletedAt: Date.now() },
    );

    // Mark in Gemini as well
    await GeminiRAGService.markDocumentDeleted(userId, documentId);
    scopedLogger.info('Document soft-deleted', { userId, documentId });
    await invalidateKnowledgeBaseCaches(userId);

    return true;
  }

  /**
   * Get documents including Gemini fields
   */
  static async getDocumentsWithGemini(
    userId: string,
    options?: {
      category?: string;
      status?: KBDocumentStatus;
      includeDeleted?: boolean;
      limit?: number;
      offset?: number;
    }
  ): Promise<{ documents: KnowledgeBaseDocumentWithGemini[]; total: number }> {
    const raw = await convexQuery<Record<string, unknown>[]>(
      'voice_agents/queries:getKBDocuments',
      stripUndefined({
        userId,
        category: options?.category,
        status: options?.status,
      }),
    );

    let allDocs = (raw || []).map((d) => convexToDoc<KnowledgeBaseDocumentWithGemini>(d)!);

    // Filter out soft-deleted unless includeDeleted
    if (!options?.includeDeleted) {
      allDocs = allDocs.filter((d) => !d.deletedAt);
    }

    // Sort by createdAt descending
    allDocs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());

    // Client-side pagination
    const offset = options?.offset || 0;
    const limit = options?.limit || 50;
    const total = allDocs.length;
    const documents = allDocs.slice(offset, offset + limit);

    return { documents, total };
  }

  /**
   * Get Gemini store statistics
   */
  static async getGeminiStats(userId: string) {
    return GeminiRAGService.getStoreStats(userId);
  }

  /**
   * Check if Gemini RAG is configured
   */
  static isGeminiConfigured(): boolean {
    return GeminiRAGService.isConfigured();
  }

  /**
   * Delete user's entire Gemini store (for account deletion)
   */
  static async deleteGeminiStore(userId: string): Promise<void> {
    return GeminiRAGService.deleteStore(userId);
  }
}
