// src/services/search.ts
// Copyright (C) 2025 DTP Technologies, LLC
// All Rights Reserved

import { google } from "googleapis";

import { DtpService } from "../lib/service.js";
import User, { IGoogleCseConnection } from "../models/user.js";

export interface SearchResult {
  title: string;
  link: string;
  snippet: string;
  image?: string;
  position?: number;
  displayLink?: string;
}

export interface SearchOptions {
  num?: number;
  siteSearch?: string;
  dateRestrict?: string;
  fileType?: string;
  safe?: "active" | "off";
  sort?: "relevance" | "date";
  start?: number;
}

export class SearchServiceError extends Error {
  constructor(
    public code: string,
    message: string,
    public details?: any,
  ) {
    super(message);
    this.name = "SearchServiceError";
  }
}

class SearchService extends DtpService {
  get name(): string {
    return "SearchService";
  }
  get slug(): string {
    return "search";
  }

  constructor() {
    super();
  }

  async start(): Promise<void> {
    this.log.info("service started");
  }

  async stop(): Promise<void> {
    this.log.info("service stopped");
  }

  /**
   * Check if a user has Google CSE credentials configured
   */
  async userHasCseConfigured(userId: string): Promise<boolean> {
    try {
      const credentials = await this.getUserCseCredentials(userId);
      return credentials !== null;
    } catch (error) {
      this.log.error("Error checking CSE configuration", { userId, error });
      return false;
    }
  }

  /**
   * Get user's CSE credentials from database
   * Note: The schema has select: false on apiKey and engineId,
   * so we need to explicitly request them
   */
  private async getUserCseCredentials(
    userId: string,
  ): Promise<IGoogleCseConnection | null> {
    const user = await User.findById(userId)
      .select("+connections.google.cse.apiKey +connections.google.cse.engineId")
      .lean();

    if (!user) {
      throw new SearchServiceError(
        "USER_NOT_FOUND",
        `User ${userId} not found`,
      );
    }

    const cseConfig = user.connections?.google?.cse;
    if (!cseConfig || !cseConfig.apiKey || !cseConfig.engineId) {
      return null;
    }

    return {
      apiKey: cseConfig.apiKey,
      engineId: cseConfig.engineId,
    };
  }

  /**
   * Test CSE credentials by performing a simple search
   */
  async testCredentials(
    apiKey: string,
    engineId: string,
  ): Promise<{ success: boolean; error?: string }> {
    try {
      const customSearch = google.customsearch({
        version: "v1",
        auth: apiKey,
      });

      // Perform a simple test search
      await customSearch.cse.list({
        q: "test",
        cx: engineId,
        num: 1,
      });

      // If we get here without error, credentials are valid
      return { success: true };
    } catch (error: any) {
      const errorInfo = this.parseCseError(error);
      return {
        success: false,
        error: errorInfo.message,
      };
    }
  }

  /**
   * Search for a user using their CSE credentials
   */
  async searchForUser(
    userId: string,
    query: string,
    options: SearchOptions = {},
  ): Promise<SearchResult[]> {
    const credentials = await this.getUserCseCredentials(userId);

    if (!credentials) {
      throw new SearchServiceError(
        "CSE_NOT_CONFIGURED",
        "Google Custom Search is not configured for this user. " +
          "Please configure your Google CSE credentials in Account Settings.",
        {
          recoveryHint: "Press Ctrl+, to open Account Settings",
        },
      );
    }

    return this.executeSearch(credentials, query, options);
  }

  /**
   * Execute search with provided credentials
   */
  private async executeSearch(
    credentials: IGoogleCseConnection,
    query: string,
    options: SearchOptions = {},
  ): Promise<SearchResult[]> {
    const customSearch = google.customsearch({
      version: "v1",
      auth: credentials.apiKey,
    });

    const params: any = {
      q: query,
      cx: credentials.engineId,
      num: options.num || 10,
    };

    if (options.siteSearch) {
      params.siteSearch = options.siteSearch;
    }
    if (options.dateRestrict) {
      params.dateRestrict = options.dateRestrict;
    }
    if (options.fileType) {
      params.fileType = options.fileType;
    }
    if (options.safe) {
      params.safe = options.safe;
    }
    if (options.sort) {
      params.sort = options.sort;
    }
    if (options.start) {
      params.start = options.start;
    }

    try {
      const response = await customSearch.cse.list(params);
      const results: SearchResult[] = [];

      if (response.data.items) {
        response.data.items.forEach((item: any, index: number) => {
          const result: SearchResult = {
            title: item.title || "",
            link: item.link || "",
            snippet: item.snippet || "",
            position: item.position || index + 1,
            displayLink: item.displayLink || "",
          };

          // Extract thumbnail if available
          if (item.pagemap?.cse_thumbnail?.[0]?.src) {
            result.image = item.pagemap.cse_thumbnail[0].src;
          }

          results.push(result);
        });
      }

      return results;
    } catch (error: any) {
      this.handleCseError(error);
    }
  }

  /**
   * Legacy search method - will be removed after migration
   * For now, throws error to force migration to per-user search
   */
  async search(
    _query: string,
    _options: SearchOptions = {},
  ): Promise<SearchResult[]> {
    throw new SearchServiceError(
      "DEPRECATED_METHOD",
      "The search() method is deprecated. Use searchForUser(userId, query, options) instead.",
    );
  }

  /**
   * Legacy searchWithPagination - will be removed after migration
   */
  async searchWithPagination(
    _query: string,
    _page: number = 1,
    _resultsPerPage: number = 10,
  ): Promise<SearchResult[]> {
    throw new SearchServiceError(
      "DEPRECATED_METHOD",
      "The searchWithPagination() method is deprecated. Use searchForUser(userId, query, { start: ... }) instead.",
    );
  }

  /**
   * Parse Google CSE API errors and provide meaningful messages
   */
  private parseCseError(error: any): {
    code: string;
    message: string;
    details?: any;
  } {
    // Handle Google API error structure
    if (error.response?.data?.error) {
      const apiError = error.response.data.error;
      const statusCode = error.response.status;

      switch (statusCode) {
        case 401:
          return {
            code: "UNAUTHORIZED",
            message: `401 Unauthorized - Invalid API key. ${apiError.message || ""}`,
            details: { statusCode, apiError },
          };
        case 403:
          return {
            code: "FORBIDDEN",
            message: `403 Forbidden - ${apiError.message || "Access denied"}. Check your Engine ID and API key permissions.`,
            details: { statusCode, apiError },
          };
        case 429:
          const retryAfter = error.response.headers?.["retry-after"];
          return {
            code: "RATE_LIMIT_EXCEEDED",
            message: `429 Too Many Requests - Rate limit exceeded. ${retryAfter ? `Retry after: ${retryAfter} seconds.` : ""} ${apiError.message || ""}`,
            details: { statusCode, apiError, retryAfter },
          };
        default:
          return {
            code: `HTTP_${statusCode}`,
            message: `HTTP ${statusCode} - ${apiError.message || "Unknown error"}`,
            details: { statusCode, apiError },
          };
      }
    }

    // Handle network errors
    if (error.code === "ENOTFOUND") {
      return {
        code: "NETWORK_ERROR",
        message: "Network error - Unable to reach Google API",
        details: { code: error.code },
      };
    }

    // Generic error
    return {
      code: error.code || "UNKNOWN_ERROR",
      message: error.message || "An unknown error occurred",
      details: error,
    };
  }

  /**
   * Handle CSE errors and throw appropriate SearchServiceError
   */
  private handleCseError(error: any): never {
    const errorInfo = this.parseCseError(error);
    throw new SearchServiceError(
      errorInfo.code,
      errorInfo.message,
      errorInfo.details,
    );
  }
}

export default new SearchService();
