import { type LobeToolManifest } from '@lobechat/context-engine';
import { MarketSDK, type OrgRef, orgRefToPathSegment } from '@lobehub/market-sdk';
import debug from 'debug';
import { type NextRequest } from 'next/server';

import { type TrustedClientUserInfo } from '@/libs/trusted-client';
import { generateTrustedClientToken, getTrustedClientTokenForSession } from '@/libs/trusted-client';

const log = debug('lobe-server:market-service');

const MARKET_BASE_URL = process.env.MARKET_BASE_URL || 'https://market.lobehub.com';

// ============================== Helper Functions ==============================

/**
 * Extract access token from Authorization header
 */
export function extractAccessToken(req: NextRequest): string | undefined {
  const authHeader = req.headers.get('authorization');
  if (authHeader?.startsWith('Bearer ')) {
    return authHeader.slice(7);
  }
  return undefined;
}

export interface LobehubSkillExecuteParams {
  args: Record<string, any>;
  context?: {
    topicId?: string;
  };
  provider: string;
  toolName: string;
}

export interface LobehubSkillExecuteResult {
  content: string;
  error?: { code: string; message?: string };
  success: boolean;
}

export interface MarketServiceOptions {
  /** Access token from OIDC flow (user token) */
  accessToken?: string;
  /** Client credentials for M2M authentication */
  clientCredentials?: {
    clientId: string;
    clientSecret: string;
  };
  /**
   * Owner account id for organization-scoped operations.
   *
   * When set, Market attributes reads/writes (currently: creds and inject-creds)
   * to the given organization account instead of the actor's personal account.
   * Used by the workspace creds router after resolving a cloud workspace to
   * its Market organization via {@link WorkspaceMarketIdentityService}.
   */
  ownerAccountId?: number;
  /** Pre-generated trusted client token (alternative to userInfo) */
  trustedClientToken?: string;
  /** User info for generating trusted client token */
  userInfo?: TrustedClientUserInfo;
}

/**
 * Market Service
 *
 * Provides a unified interface to MarketSDK with business logic encapsulation.
 * This service wraps MarketSDK methods to avoid repetition across the codebase.
 *
 * Usage:
 * ```typescript
 * // From Next.js request (API Routes) - recommended
 * const marketService = await MarketService.createFromRequest(req);
 * await marketService.submitFeedback({ ... });
 *
 * // With user authentication
 * const service = new MarketService({ accessToken, userInfo });
 *
 * // With trusted client only
 * const service = new MarketService({ userInfo });
 *
 * // M2M authentication
 * const service = new MarketService({ clientCredentials: { clientId, clientSecret } });
 *
 * // Public endpoints (no auth)
 * const service = new MarketService();
 * ```
 */
export class MarketService {
  market: MarketSDK;

  constructor(options: MarketServiceOptions = {}) {
    const { accessToken, userInfo, clientCredentials, trustedClientToken, ownerAccountId } =
      options;

    // Use provided trustedClientToken or generate from userInfo
    const resolvedTrustedClientToken =
      trustedClientToken || (userInfo ? generateTrustedClientToken(userInfo) : undefined);

    this.market = new MarketSDK({
      accessToken,
      baseURL: MARKET_BASE_URL,
      clientId: clientCredentials?.clientId,
      clientSecret: clientCredentials?.clientSecret,
      ownerAccountId,
      trustedClientToken: resolvedTrustedClientToken,
    });

    log(
      'MarketService initialized: baseURL=%s, hasAccessToken=%s, hasTrustedToken=%s, hasClientCredentials=%s, ownerAccountId=%s',
      MARKET_BASE_URL,
      !!accessToken,
      !!resolvedTrustedClientToken,
      !!clientCredentials,
      ownerAccountId ?? 'none',
    );
  }

  // ============================== Factory Methods ==============================

  /**
   * Create MarketService from Next.js request (server-side only)
   * Extracts accessToken from Authorization header and trustedClientToken from session
   */
  static async createFromRequest(req: NextRequest): Promise<MarketService> {
    const accessToken = extractAccessToken(req);
    const trustedClientToken = await getTrustedClientTokenForSession();

    return new MarketService({
      accessToken,
      trustedClientToken,
    });
  }

  // ============================== Feedback Methods ==============================

  /**
   * Submit feedback to LobeHub
   */
  async submitFeedback(params: {
    clientInfo?: {
      language?: string;
      timezone?: string;
      url?: string;
      userAgent?: string;
    };
    email?: string;
    message: string;
    screenshotUrl?: string;
    title: string;
  }) {
    const { title, message, email, screenshotUrl, clientInfo } = params;

    // Build message with screenshot if available
    let feedbackMessage = message;
    if (screenshotUrl) {
      feedbackMessage += `\n\n**Screenshot**: ${screenshotUrl}`;
    }

    return this.market.feedback.submitFeedback({
      clientInfo,
      email: email || '',
      message: feedbackMessage,
      title,
    });
  }

  // ============================== Auth Methods ==============================

  /**
   * Exchange OAuth authorization code for tokens
   */
  async exchangeAuthorizationCode(params: {
    clientId: string;
    code: string;
    codeVerifier: string;
    redirectUri: string;
  }) {
    return this.market.auth.exchangeOAuthToken({
      clientId: params.clientId,
      code: params.code,
      codeVerifier: params.codeVerifier,
      grantType: 'authorization_code',
      redirectUri: params.redirectUri,
    });
  }

  /**
   * Get OAuth handoff information
   */
  async getOAuthHandoff(id: string) {
    return this.market.auth.getOAuthHandoff(id);
  }

  /**
   * Get user info from token
   */
  async getUserInfo(token: string) {
    return this.market.auth.getUserInfo(token);
  }

  /**
   * Get user info with trusted client token (server-side)
   */
  async getUserInfoWithTrustedClient() {
    const userInfoUrl = `${MARKET_BASE_URL}/lobehub-oidc/userinfo`;
    const response = await fetch(userInfoUrl, {
      // @ts-ignore
      headers: this.market.headers,
      method: 'GET',
    });

    if (!response.ok) {
      throw new Error('Failed to get user info');
    }

    return response.json();
  }

  /**
   * Refresh OAuth token
   */
  async refreshToken(params: { clientId: string; refreshToken: string }) {
    return this.market.auth.exchangeOAuthToken({
      clientId: params.clientId,
      grantType: 'refresh_token',
      refreshToken: params.refreshToken,
    });
  }

  // ============================== Client Methods ==============================

  /**
   * Register client for M2M authentication
   */
  async registerClient(params: {
    clientName: string;
    clientType: string;
    deviceId: string;
    platform?: string;
    version?: string;
  }) {
    // @ts-ignore
    return this.market.registerClient(params);
  }

  /**
   * Fetch M2M token with client credentials
   */
  async fetchM2MToken() {
    return this.market.fetchM2MToken();
  }

  // ============================== Skills Methods ==============================

  /**
   * List available tools for a provider
   */
  async listSkillTools(providerId: string) {
    return this.market.skills.listTools(providerId);
  }

  /**
   * Call a skill tool
   */
  async callSkillTool(provider: string, params: { args: Record<string, any>; tool: string }) {
    return this.market.skills.callTool(provider, params);
  }

  /**
   * List user's connected skills
   */
  async listSkillConnections() {
    return this.market.connect.listConnections();
  }

  // ============================== Plugin Methods ==============================

  /**
   * Call cloud MCP endpoint
   */
  async callCloudMcpEndpoint(
    params: {
      apiParams: Record<string, any>;
      identifier: string;
      toolName: string;
    },
    options?: {
      headers?: Record<string, string>;
    },
  ) {
    return this.market.plugins.callCloudGateway(params, options);
  }

  /**
   * Export file from sandbox to upload URL
   */
  async exportFile(params: { path: string; topicId: string; uploadUrl: string; userId: string }) {
    const { path, uploadUrl, topicId, userId } = params;

    return this.market.plugins.runBuildInTool(
      'exportFile',
      { path, uploadUrl },
      { topicId, userId },
    );
  }

  /**
   * Get plugin manifest
   */
  async getPluginManifest(params: {
    identifier: string;
    install?: boolean;
    locale?: string;
    version?: string;
  }) {
    return this.market.plugins.getPluginManifest(params);
  }

  /**
   * Report plugin installation
   */
  async reportPluginInstallation(params: any) {
    return this.market.plugins.reportInstallation(params);
  }

  /**
   * Report plugin call
   */
  async reportPluginCall(params: any) {
    return this.market.plugins.reportCall(params);
  }

  /**
   * Create plugin event
   */
  async createPluginEvent(params: any) {
    return this.market.plugins.createEvent(params);
  }

  // ============================== Agent Methods ==============================

  /**
   * Get agent detail
   */
  async getAgentDetail(identifier: string, options?: { locale?: string; version?: string }) {
    return this.market.agents.getAgentDetail(identifier, options);
  }

  /**
   * Get agent list
   */
  async getAgentList(params?: any) {
    return this.market.agents.getAgentList(params);
  }

  /**
   * Increase agent install count
   */
  async increaseAgentInstallCount(identifier: string) {
    return this.market.agents.increaseInstallCount(identifier);
  }

  /**
   * Create agent event
   */
  async createAgentEvent(params: any) {
    return this.market.agents.createEvent(params);
  }

  // ============================== Agent Group Methods ==============================

  /**
   * Get agent group detail
   */
  async getAgentGroupDetail(identifier: string, options?: { locale?: string; version?: number }) {
    return this.market.agentGroups.getAgentGroupDetail(identifier, options);
  }

  /**
   * Get agent group list
   */
  async getAgentGroupList(params?: any) {
    return this.market.agentGroups.getAgentGroupList(params);
  }

  // ============================== User Methods ==============================

  /**
   * Get user profile by username
   */
  async getUserProfile(username: string, options?: { locale?: string }) {
    return this.market.user.getUserInfo(username, options);
  }

  /**
   * Register user on market and optionally follow another user
   */
  async registerUser(params: { followUserId?: string; registerUserId: string }): Promise<void> {
    await this.market.user.register(params);
  }

  // ============================== Skills Methods (using SDK) ==============================

  /**
   * Search for skills in the LobeHub Market
   */
  async searchSkill(params: {
    category?: string;
    locale?: string;
    order?: 'asc' | 'desc';
    page?: number;
    pageSize?: number;
    q?: string;
    sort?:
      | 'createdAt'
      | 'forks'
      | 'installCount'
      | 'name'
      | 'relevance'
      | 'stars'
      | 'updatedAt'
      | 'watchers';
  }) {
    log('searchSkill: %O', params);

    const result = await this.market.marketSkills.getSkillList(params);

    log('searchSkill response: %O', result);

    return result;
  }

  /**
   * Get skill detail from market
   */
  async getSkillDetail(identifier: string, options?: { locale?: string; version?: string }) {
    log('getSkillDetail: %s, options: %O', identifier, options);

    const result = await this.market.marketSkills.getSkillDetail(identifier, options);

    log('getSkillDetail response: %O', result);

    return result;
  }

  /**
   * Get skill download URL from market
   */
  getSkillDownloadUrl(identifier: string, version?: string): string {
    return this.market.marketSkills.getDownloadUrl(identifier, version);
  }

  /**
   * Download skill ZIP directly
   */
  async downloadSkill(identifier: string, version?: string) {
    log('downloadSkill: %s, version: %s', identifier, version);

    return this.market.marketSkills.downloadSkill(identifier, version);
  }

  /**
   * Get skill categories
   */
  async getSkillCategories() {
    log('getSkillCategories');

    return this.market.marketSkills.getCategories();
  }

  /**
   * Execute a LobeHub Skill tool
   * @param params - The skill execution parameters (provider, toolName, args)
   * @returns Execution result with content and success status
   */
  async executeLobehubSkill(params: LobehubSkillExecuteParams): Promise<LobehubSkillExecuteResult> {
    const { provider, toolName, args, context } = params;

    log('executeLobehubSkill: %s/%s with args: %O, context: %O', provider, toolName, args, context);

    try {
      const response = await this.market.skills.callTool(provider, {
        args,
        // @ts-ignore
        topicId: context?.topicId,
        tool: toolName,
      });

      log('executeLobehubSkill: response: %O', response);

      return {
        content: typeof response.data === 'string' ? response.data : JSON.stringify(response.data),
        success: response.success,
      };
    } catch (error) {
      const err = error as Error;
      console.error('MarketService.executeLobehubSkill error %s/%s: %O', provider, toolName, err);

      // MarketAPIError carries the full error response body from the API,
      // including structured details (command, exitCode, stdout, stderr).
      // Extract it so the content is not empty on failure.
      const errorBody = (err as any).errorBody;
      const skillError = errorBody?.error;
      const content = skillError ? JSON.stringify(skillError) : err.message;

      return {
        content,
        error: {
          code: skillError?.code || 'LOBEHUB_SKILL_ERROR',
          message: skillError?.message || err.message,
        },
        success: false,
      };
    }
  }

  /**
   * Fetch LobeHub Skills manifests from Market API
   * Gets user's connected skills and builds tool manifests for agent execution
   *
   * @returns Array of tool manifests for connected skills
   */
  async getLobehubSkillManifests(): Promise<LobeToolManifest[]> {
    try {
      // 1. Get user's connected skills
      const { connections } = await this.market.connect.listConnections();
      if (!connections || connections.length === 0) {
        log('getLobehubSkillManifests: no connected skills found');
        return [];
      }

      log('getLobehubSkillManifests: found %d connected skills', connections.length);

      // 2. Fetch tools for each connection and build manifests
      const manifests: LobeToolManifest[] = [];

      for (const connection of connections) {
        try {
          // Connection returns providerId (e.g., 'twitter', 'linear'), not numeric id
          const providerId = (connection as any).providerId;
          if (!providerId) {
            log('getLobehubSkillManifests: connection missing providerId: %O', connection);
            continue;
          }
          const icon = (connection as any).icon;

          // Look up the provider's display name from the static registry.
          // connection.providerName is the *user's* display name on that provider,
          // NOT the provider's own name (e.g., "LiJian" instead of "Linear").
          // Static label map — avoids importing LOBEHUB_SKILL_PROVIDERS which
          // pulls in react-icons (client-side only). Keep in sync with lobehubSkill.ts.
          const PROVIDER_LABELS: Record<string, string> = {
            github: 'GitHub',
            linear: 'Linear',
            microsoft: 'Outlook Calendar',
            notion: 'Notion',
            twitter: 'X (Twitter)',
            vercel: 'Vercel',
          };
          const providerLabel = PROVIDER_LABELS[providerId] || providerId;

          const { tools, instruction } = await this.market.skills.listTools(providerId);
          if (!tools || tools.length === 0) continue;

          const manifest: LobeToolManifest = {
            api: tools.map((tool: any) => ({
              description: tool.description || '',
              name: tool.name,
              parameters: tool.inputSchema || { properties: {}, type: 'object' },
            })),
            identifier: providerId,
            meta: {
              avatar: icon || '🔗',
              description: `LobeHub Skill: ${providerLabel}`,
              tags: ['lobehub-skill', providerId],
              title: providerLabel,
            },
            systemRole: instruction || undefined,
            type: 'builtin',
          };

          manifests.push(manifest);
          log(
            'getLobehubSkillManifests: built manifest for %s with %d tools',
            providerId,
            tools.length,
          );
        } catch (error) {
          log('getLobehubSkillManifests: failed to fetch tools for connection: %O', error);
        }
      }

      return manifests;
    } catch (error) {
      log('getLobehubSkillManifests: error fetching skills: %O', error);
      return [];
    }
  }

  // ============================== Creds Methods ==============================

  /**
   * Upload a credential file to Market.
   *
   * The SDK doesn't expose multipart upload so this method calls the REST
   * endpoint directly. Pass `orgId` to upload to an organization's cred
   * bucket (`/api/v1/organizations/:orgId/creds/upload`); omit it for a
   * personal upload (`/api/v1/user/creds/upload`).
   *
   * @param params.file - File content as base64 string
   * @param params.fileName - Original file name
   * @param params.fileType - MIME type of the file
   * @param params.orgId - Optional organization account id. When set, the
   *   upload is attributed to the org via the org-scoped URL; org membership
   *   (admin) is enforced server-side by `requireOrgMembership`.
   * @returns Upload result with fileHashId
   */
  async uploadCredFile(params: {
    file: string; // base64 encoded file content
    fileName: string;
    fileType: string;
    orgId?: OrgRef;
  }): Promise<{ fileHashId: string; fileName: string; fileSize: number; fileType: string }> {
    const { file, fileName, fileType, orgId } = params;
    // Numeric account id or `workspace:<workspaceId>` path segment.
    const orgSegment = orgId === undefined ? undefined : orgRefToPathSegment(orgId);

    log(
      'uploadCredFile: fileName=%s, fileType=%s, orgId=%s',
      fileName,
      fileType,
      orgSegment ?? 'none',
    );

    // Convert base64 to Blob
    const binaryString = atob(file);
    const bytes = new Uint8Array(binaryString.length);
    for (let i = 0; i < binaryString.length; i++) {
      bytes[i] = binaryString.charCodeAt(i);
    }
    const blob = new Blob([bytes], { type: fileType });

    // Create FormData
    const formData = new FormData();
    formData.append('file', blob, fileName);

    // Extract only auth headers (not Content-Type, which would break multipart/form-data).
    // We deliberately also strip `x-lobe-owner-account-id` for the org path —
    // ownership is in the URL now, the header is ignored by the org route.
    // @ts-ignore - market.headers contains auth headers
    const sdkHeaders = this.market.headers as Record<string, string>;
    const authHeaders: Record<string, string> = {};
    for (const [key, value] of Object.entries(sdkHeaders)) {
      const lower = key.toLowerCase();
      if (lower === 'content-type') continue;
      if (lower === 'x-lobe-owner-account-id' && orgSegment !== undefined) continue;
      authHeaders[key] = value;
    }

    // Call Market API directly
    const uploadPath =
      orgSegment === undefined
        ? '/api/v1/user/creds/upload'
        : `/api/v1/organizations/${orgSegment}/creds/upload`;
    const uploadUrl = `${MARKET_BASE_URL}${uploadPath}`;
    const response = await fetch(uploadUrl, {
      body: formData,
      headers: authHeaders,
      method: 'POST',
    });

    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      log('uploadCredFile error: %O', errorData);
      throw new Error(errorData.message || `Upload failed with status ${response.status}`);
    }

    const result = await response.json();
    log('uploadCredFile success: fileHashId=%s', result.fileHashId);
    return result;
  }

  // ============================== Direct SDK Access ==============================

  /**
   * Get MarketSDK instance for advanced usage
   * Use this when you need direct access to SDK methods not wrapped by this service
   */
  getSDK(): MarketSDK {
    return this.market;
  }
}
