import { Injectable, OnModuleInit } from "@nestjs/common";
import { Request, Response } from "express";
import { z } from "zod";
import crypto from "crypto";
import { existsSync, readFileSync } from "fs";
import { join } from "path";
import { AsyncLocalStorage } from "async_hooks";
import { JwtService } from "@nestjs/jwt";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

import { BrowseInfluencerService } from "src/modules/browse-influencer/browse-influencer.services";
import { Users } from "src/entities/users.entity";

type ViewerType = "guest" | "member";
type McpRequestContext = {
  viewerType: ViewerType;
  userId?: string;
};

@Injectable()
export class InfluencerMcpService implements OnModuleInit {
  private mcpServer: McpServer;
  private readonly requestContextStore = new AsyncLocalStorage<McpRequestContext>();

  constructor(
    private readonly browseInfluencerService: BrowseInfluencerService,
    private readonly jwtService: JwtService,
    @InjectRepository(Users)
    private readonly usersRepository: Repository<Users>
  ) { }

  private extractBearerToken(req: Request): string | null {
    const raw = req.headers?.authorization;
    if (!raw || typeof raw !== "string") return null;
    if (!raw.toLowerCase().startsWith("bearer ")) return null;
    const token = raw.slice(7).trim();
    return token || null;
  }

  private async resolveRequestContext(req: Request): Promise<McpRequestContext> {
    const token = this.extractBearerToken(req);
    if (!token) return { viewerType: "guest" };

    try {
      const payload = await this.jwtService.verifyAsync<any>(token);
      const userId = payload?.sub;
      if (payload?.mcp_member === true) {
        return { viewerType: "member", userId: typeof userId === "string" ? userId : undefined };
      }
      if (!userId) return { viewerType: "guest" };

      const user = await this.usersRepository.findOne({
        where: { id: userId },
        select: ["id", "is_active"],
      });

      if (!user || !user.is_active) return { viewerType: "guest" };
      return { viewerType: "member", userId: user.id };
    } catch (error) {
      return { viewerType: "guest" };
    }
  }

  private getViewerTypeFromContext(): ViewerType {
    const context = this.requestContextStore.getStore();
    console.table(context);
    return context?.viewerType === "member" ? "member" : "guest";
  }

  private getCategoryTitles(item: any): string[] {
    const categories = item?.influencerCategories || [];
    return categories
      .map(
        (ic: any) =>
          ic?.category?.translations?.[0]?.title ?? ic?.category?.title
      )
      .filter(Boolean);
  }

  private getPlatforms(item: any): string[] {
    if (!Array.isArray(item?.platforms)) return [];

    return item.platforms
      .map((platform: any) => String(platform ?? "").trim())
      .filter(Boolean)
      .map((platform: string) => {
        const normalized = platform.toLowerCase();
        if (normalized === "youtube") return "YouTube";
        if (normalized === "tiktok") return "TikTok";
        if (normalized === "xiaohongshu") return "Xiaohongshu";
        return normalized.charAt(0).toUpperCase() + normalized.slice(1);
      });
  }

  private getTags(item: any): string[] {
    const userKeywords = item?.userKeywords || [];
    return userKeywords
      .map((uk: any) => {
        const translations = uk?.keywords?.translations || [];
        return (
          translations?.[0]?.keyword ||
          uk?.keywords?.keyword ||
          uk?.keywords?.title ||
          null
        );
      })
      .filter(Boolean);
  }

  private getSocialHandles(item: any): Array<{ platform: string; handle: string }> {
    const socialMedias = item?.social_medias || [];
    return socialMedias
      .map((social: any) => {
        const platform = String(social?.social_type || "").trim();
        const handle =
          String(
            social?.social_user_name ||
            social?.display_name ||
            social?.city_name ||
            ""
          ).trim();
        if (!platform || !handle) return null;
        return { platform, handle };
      })
      .filter(Boolean);
  }

  private isUuidString(s: string): boolean {
    return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
      s
    );
  }

  /** Normalize profile/post paths to absolute URLs for the ChatGPT widget. */
  private resolveMediaUrl(path: string | null | undefined): string {
    if (!path || typeof path !== "string") return "";
    const trimmed = path.trim();
    if (!trimmed) return "";
    if (/^https?:\/\//i.test(trimmed)) return trimmed;

    const base = String(process.env.AWS_FILE_PATH || "").replace(/\/?$/, "/");
    if (!base) return trimmed;

    if (trimmed.startsWith("images/") || trimmed.startsWith("files/")) {
      return `${base}${trimmed}`;
    }

    const fileName = trimmed.includes("/")
      ? trimmed.split("/").pop() || trimmed
      : trimmed;
    return `${base}images/profile/${fileName}`;
  }

  /**
   * CSP must live on resource contents `_meta.ui.csp` (not registerAppResource config).
   * ChatGPT merges resourceDomains into img-src for the widget iframe.
   */
  private getMcpWidgetUiMeta() {
    const resourceDomains = new Set<string>([
      "https://*.cloudfront.net",
      "https://*.cdninstagram.com",
      "https://*.fbcdn.net",
      "https://*.tiktokcdn.com",
      "https://*.tiktokcdn-us.com",
      "https://*.tiktokv.com",
      "https://*.googleusercontent.com",
      "https://*.ggpht.com",
      "https://*.ytimg.com",
      "https://*.instagram.com",
      "https://*.facebook.com",
      "https://*.tiktok.com",
      "https://*.tiktok.com",
    ]);

    const filePath = String(process.env.AWS_FILE_PATH || "").trim();
    if (filePath.startsWith("http")) {
      try {
        resourceDomains.add(new URL(filePath).origin);
      } catch {
        /* ignore invalid AWS_FILE_PATH */
      }
    }

    const connectDomains = new Set<string>();
    const mcpBase = String(process.env.MCP_PUBLIC_BASE_URL || "").trim();
    if (mcpBase.startsWith("http")) {
      try {
        connectDomains.add(new URL(mcpBase).origin);
      } catch {
        /* ignore */
      }
    }

    return {
      ui: {
        prefersBorder: true,
        csp: {
          resourceDomains: [...resourceDomains],
          connectDomains: connectDomains.size ? [...connectDomains] : [],
        },
      },
    };
  }

  private topPostsToStructured(topPosts: any[]) {
    return (topPosts || []).map((p: any, index: number) => {
      const likes = Number(p?.like_count ?? 0);
      const comments = Number(p?.comment_count ?? 0);
      return {
        rank: index + 1,
        id: p?.id ?? "",
        social_type: p?.social_type ?? "",
        title: p?.title ?? "",
        description: p?.description ?? "",
        image_url: this.resolveMediaUrl(p?.image_url),
        post_url: p?.post_url ?? "",
        like_count: likes,
        comment_count: comments,
        engagement_total: likes + comments,
        created_time: p?.created_time ?? "",
      };
    });
  }

  private formatCards(results: any[], viewerType: ViewerType) {
    const lockedText =
      viewerType === "guest"
        ? "Profile: Locked (preview-only for guests). Login to unlock."
        : "Profile: Unlocked";

    return (results || []).map((item: any, index: number) => {
      const displayName =
        item?.display_name ||
        `${item?.first_name || ""} ${item?.last_name || ""}`.trim() ||
        "Unknown";

      const categoryTitles = this.getCategoryTitles(item);
      const primary = categoryTitles[0] ?? "General";
      const moreCount = Math.max(0, categoryTitles.length - 1);
      const tags = this.getTags(item);
      const socialHandles = this.getSocialHandles(item);

      const followers = Number(item?.total_followers || 0).toLocaleString(
        "en-US"
      );
      const engagement = item?.engagement_ratio ?? "N/A";
      const platformList = this.getPlatforms(item);

      return {
        id: item?.id ?? String(index),
        slug: item?.slug ?? "",
        display_name: displayName,
        country: item?.country ?? "N/A",
        image: this.resolveMediaUrl(item?.image),
        categoryPrimary: primary,
        categoryMoreCount: moreCount,
        categories: categoryTitles,
        tags,
        platforms: platformList,
        platforms_text: platformList.length ? platformList.join(", ") : "N/A",
        social_handles: socialHandles,
        about_me: item?.about_me ?? "",
        total_posts_text: String(item?.total_posts ?? "N/A"),
        total_followers_text: followers,
        engagement_text: String(engagement),
        lockedText,
        user_profile_url: process.env.FRONTEND_URL + "influencers/" + item?.slug,
      };
    });
  }

  private formatFallbackText(
    results: any[],
    viewerType: ViewerType
  ): string {
    const isGuest = viewerType === "guest";
    const header = isGuest ? "Guest Preview: Influencer List" : "Influencer List";
    const footer = isGuest
      ? "\n\nProfile links are locked for guests. Login to unlock: https://app.connector.cc/login"
      : "";

    if (!results?.length) return `${header}\n\nNo influencers found.${footer}`;

    const rows = results.map((item: any, index: number) => {
      const displayName =
        item?.display_name ||
        `${item?.first_name || ""} ${item?.last_name || ""}`.trim() ||
        "Unknown";

      const country = item?.country || "N/A";

      const primaryCategory =
        (item?.influencerCategories || [])
          .map(
            (ic: any) =>
              ic?.category?.translations?.[0]?.title ?? ic?.category?.title
          )
          .filter(Boolean)[0] ?? "General";

      const moreCount = Math.max(
        0,
        (item?.influencerCategories || []).length - 1
      );
      const moreText = moreCount > 0 ? ` +${moreCount} more` : "";

      const username = item?.slug ? `@${item.slug}` : "N/A";
      const followers = Number(item?.total_followers || 0).toLocaleString(
        "en-US"
      );
      const engagement = item?.engagement_ratio ?? "N/A";
      const imageUrl = this.resolveMediaUrl(item?.image);
      const imageMarkdown = imageUrl ? `![${displayName}](${imageUrl})` : "";
      const lockText = isGuest ? "Profile: Locked" : "Profile: Unlocked";

      const platformLine =
        Array.isArray(item?.platforms) && item.platforms.length
          ? item.platforms.join(", ")
          : "Platform N/A";

      return [
        `${index + 1}. ${imageMarkdown ? imageMarkdown + " " : ""}**${displayName}**`,
        `   ${username} | ${platformLine}`,
        `   ${country} • ${primaryCategory}${moreText}`,
        `   Followers: ${followers} | ER: ${engagement}`,
        `   ${lockText}`,
      ]
        .filter(Boolean)
        .join("\n");
    });

    return `${header}\n\n${rows.join("\n\n")}${footer}`;
  }

  private getInfluencerCardsWidgetHtml(): string {
    const candidatePaths = [
      join(process.cwd(), "src", "common", "services", "mcp", "influencer-cards.widget.html"),
      join(__dirname, "influencer-cards.widget.html"),
    ];

    for (const filePath of candidatePaths) {
      if (existsSync(filePath)) {
        return readFileSync(filePath, "utf8");
      }
    }

    throw new Error("Unable to load influencer cards widget HTML template.");
  }


  async onModuleInit() {

    const extApps = await new Function(
      "specifier",
      "return import(specifier)"
    )("@modelcontextprotocol/ext-apps/server");

    const {
      registerAppResource,
      registerAppTool,
      RESOURCE_MIME_TYPE,
    } = extApps as any;
    // register the mcp server
    this.mcpServer = new McpServer({
      name: "connectorclub-influencer-mcp",
      version: "1.0.0",
    });

    // ---------------------------------------------------------
    // RESOURCE: Influencer cards widget
    // ---------------------------------------------------------
    const widgetUri = "ui://widget/influencer-cards-v2.html";

    registerAppResource(
      this.mcpServer as any,
      "influencer-cards-widget",
      widgetUri,
      {
        description: "Influencer cards widget",
      },
      async () => ({
        contents: [
          {
            uri: widgetUri,
            mimeType: RESOURCE_MIME_TYPE,
            text: this.getInfluencerCardsWidgetHtml(),
            _meta: this.getMcpWidgetUiMeta(),
          },
        ],
      })
    );


    // ---------------------------------------------------------
    // TOOL: search_influencers
    // ---------------------------------------------------------
    registerAppTool(
      this.mcpServer as any,
      "search_influencers",
      {
        title: "Search influencers",
        inputSchema: {
          query: z
            .string()
            .optional()
            .describe(
              "Search term for influencer (name,display name,user name,about me,categories,keywords,city name,description etc.)"
            ),
          category: z
            .string()
            .optional()
            .describe("Influencer category e.g. 'fashion,fitness'"),
          platforms: z
            .array(z.object({ value: z.string(), label: z.string() }))
            .optional()
            .describe(
              "Platform e.g. 'instagram', 'tiktok', 'facebook', 'youtube'"
            ),
          country: z
            .string()
            .optional()
            .describe("Country or city e.g. 'IN', 'HK', 'US'"),
          sort: z
            .enum(["ASC", "DESC"])
            .optional()
            .default("DESC")
            .describe("DESC = top/highest, ASC = least/lowest"),
          min_followers: z
            .number()
            .optional()
            .describe("Minimum number of followers"),
          max_followers: z
            .number()
            .optional()
            .describe("Maximum number of followers"),
          min_engagement: z
            .number()
            .optional()
            .describe("Minimum engagement ratio"),
          max_engagement: z
            .number()
            .optional()
            .describe("Maximum engagement ratio"),
          gender: z
            .enum(["male", "female", "other"])
            .optional()
            .describe("Gender e.g. 'male', 'female', 'other'"),
          sort_by: z
            .enum(["total_followers", "engagement_ratio", "created_at", "display_name"])
            .optional()
            .default("total_followers")
            .describe("Column to sort by"),
          viewer_type: z
            .enum(["guest", "member"])
            .optional()
            .default("guest")
            .describe("Use 'guest' for limited preview results"),
          limit: z
            .number()
            .int()
            .positive()
            .max(50)
            .optional()
            .default(5)
            .describe(
              "Maximum number of influencers to return (default 5, max 50)."
            ),
        },
        outputSchema: z.object({
          influencers: z.array(
            z.object({
              id: z.string().optional(),
              display_name: z.string().optional(),
              username: z.string().optional(),
              platform: z.string().optional(),
              total_followers: z.number().optional(),
              engagement_ratio: z.number().optional(),
              top_posts: z.array(
                z.object({
                  id: z.string().optional(),
                  content: z.string().optional(),
                  likes: z.number().optional(),
                  comments: z.number().optional(),
                  media_url: z.string().optional(),
                })
              ).optional(),
            })
          ),
          hasResponse: z.boolean(),
          viewer_type: z.enum(["guest", "member"]),
          login_url: z.string().optional(),
        }),
        _meta: {
          "openai/outputTemplate": widgetUri,
          ui: this.getMcpWidgetUiMeta().ui,
        },
        annotations: {
          "readOnlyHint": true,
          "openWorldHint": true,
          "destructiveHint": false
        },
      },
      async ({ query, category, platforms, country, sort, sort_by, limit, viewer_type, min_followers, max_followers, min_engagement, max_engagement, gender }: any) => {
        const viewerType = this.getViewerTypeFromContext();
        const parsedLimit = Number.isFinite(limit) ? Number(limit) : 5;
        const requestedLimit = Math.max(1, Math.floor(parsedLimit || 5));
        const effectiveLimit =
          viewerType === "member"
            ? Math.min(requestedLimit, 50)
            : Math.min(requestedLimit, 5);

        const queryParam = {
          search: query ?? null,
          category: category || null,
          platforms: platforms || null,
          country: country || null,
          gender: gender || null,
          min_followers: min_followers || null,
          max_followers: max_followers || null,
          min_engagement: min_engagement || null,
          max_engagement: max_engagement || null,
          sort: sort || "DESC",
          sort_by: sort_by || "total_followers",
          limit: effectiveLimit,
          page: 1,
          per_page: effectiveLimit,
        };

        console.log("[MCP] search_influencers queryParam:", queryParam);

        const results = await this.browseInfluencerService.chatgptSearch(queryParam);

        console.log("[MCP] search_influencers raw results count:", results?.length || 0);

        const cards = this.formatCards(results || [], viewerType);
        const influencers = await Promise.all(
          cards.map(async (card) => {
            const id = String(card?.id ?? "");
            if (!this.isUuidString(id)) {
              return { ...card, top_posts: [] };
            }
            const rows = await this.browseInfluencerService.getTopSocialPostsForUser(id, 10);
            return { ...card, top_posts: this.topPostsToStructured(rows) };
          })
        );
        const fallbackText = this.formatFallbackText(results || [], viewerType);

        console.log("[MCP] search_influencers structured influencers count:", influencers.length);

        return {
          content: [
            {
              type: "text",
              text: fallbackText,
            },
          ],
          structuredContent: {
            influencers,
            hasResponse: true,
            viewer_type: viewerType,
            login_url:
              viewerType === "guest" ? process.env.MCP_FRONTEND_OAUTH_AUTHORIZE_URL + "register" : "",
          },
        };
      }
    );

    // ---------------------------------------------------------
    // TOOL: show_influencer_details
    // ---------------------------------------------------------
    // registerAppTool(
    //   this.mcpServer as any,
    //   "show_influencer_details",
    //   {
    //     title: "Show influencer details",
    //     inputSchema: {
    //       slug: z
    //         .string()
    //         .optional()
    //         .describe("Influencer profile slug (username), e.g. from search results"),
    //       influencer_id: z
    //         .string()
    //         .uuid()
    //         .optional()
    //         .describe("Influencer user id (UUID) if slug is unknown"),
    //     },
    //     annotations: {
    //       readOnlyHint: true,
    //     },
    //     _meta: {
    //       "openai/outputTemplate": "ui://widget/influencer-cards.html",
    //       "openai/toolInvocation/invoking": "Loading profile…",
    //       "openai/toolInvocation/invoked": "Profile loaded",
    //     },
    //   },
    //   async ({ slug, influencer_id }: { slug?: string; influencer_id?: string }) => {
    //     const viewerType = this.getViewerTypeFromContext()
    //     const detail = await this.browseInfluencerService.chatgptInfluencerDetail({
    //       slug: slug ?? null,
    //       id: influencer_id ?? null,
    //     })

    //     if (!detail) {
    //       const msg =
    //         "Influencer not found. Pass a valid slug (from search) or influencer_id (UUID)."
    //       return {
    //         content: [{ type: "text", text: msg }],
    //         structuredContent: {
    //           influencer: null,
    //           top_posts: [],
    //           hasResponse: false,
    //         },
    //       }
    //     }

    //     const { influencer, topPosts } = detail
    //     const [card] = this.formatCards([influencer], viewerType)

    //     const top_posts = this.topPostsToStructured(topPosts || [])

    //     const displayName =
    //       influencer?.display_name ||
    //       `${influencer?.first_name || ""} ${influencer?.last_name || ""}`.trim() ||
    //       "Unknown"
    //     const postLines = top_posts.length
    //       ? top_posts
    //         .map(
    //           (p) =>
    //             `${p.rank}. [${p.social_type}] ${p.title || "(no title)"} — ${p.engagement_total.toLocaleString("en-US")} engagements (likes+comments)${p.post_url ? ` — ${p.post_url}` : ""}`
    //         )
    //         .join("\n")
    //       : "No ranked posts available."

    //     const fallbackText = [
    //       `**${displayName}**`,
    //       card?.slug ? `@${card.slug}` : "",
    //       card?.country ? `Country: ${card.country}` : "",
    //       card?.platforms_text ? `Platforms: ${card.platforms_text}` : "",
    //       card?.lockedText ?? "",
    //       "",
    //       "**Top 10 posts (by likes + comments):**",
    //       postLines,
    //     ]
    //       .filter(Boolean)
    //       .join("\n")

    //     return {
    //       content: [{ type: "text", text: fallbackText }],
    //       structuredContent: {
    //         influencer: { ...card, top_posts },
    //         top_posts,
    //         hasResponse: true,
    //       },
    //     }
    //   }
    // )

  }

  // ---------------------------------------------------------
  // HTTP Handler
  // ---------------------------------------------------------
  async handleMcpRequest(req: Request, res: Response): Promise<void> {
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined,
      enableJsonResponse: true,
    });

    res.on("close", () => {
      transport.close();
    });

    const requestContext = await this.resolveRequestContext(req);
    await this.requestContextStore.run(requestContext, async () => {
      await this.mcpServer.connect(transport);
      await transport.handleRequest(req as any, res as any, req.body);
    });
  }
}