import { BoxIcon } from "lucide-react";
import { useState } from "react";
import type { IntegrationPlugin } from "@executor-js/sdk/client";
import { getDomain } from "tldts";

// ---------------------------------------------------------------------------
// IntegrationFavicon — renders a small favicon derived from an integration URL.
// Falls back to a neutral icon if the URL is missing or the image fails to load.
// ---------------------------------------------------------------------------

const integrationFaviconDomain = (url: string | undefined): string | null => {
  if (!url) return null;
  return getDomain(url) ?? (URL.canParse(url) ? getDomain(new URL(url).hostname) : null);
};

// integrations.sh/logo proxies context.dev's Logo Link behind an edge cache
// and is executor's single logo source; Google's favicon service remains in
// the cascade as the fallback the <img> onError walks to when the proxy is
// unreachable or serves nothing for the domain.
export function integrationFaviconUrl(url: string | undefined, size: number): string | null {
  const domain = integrationFaviconDomain(url);
  if (!domain) return null;
  return `https://integrations.sh/logo/${domain}?sz=${size * 2}`;
}

export function integrationFaviconFallbackUrl(
  url: string | undefined,
  size: number,
): string | null {
  const domain = integrationFaviconDomain(url);
  if (!domain) return null;
  return `https://www.google.com/s2/favicons?domain=${domain}&sz=${size * 2}`;
}

export function integrationLocalIconUrl(sourceId: string | undefined): string | null {
  if (sourceId !== "executor") return null;
  return "/favicon-32.png";
}

const KIND_TO_PLUGIN_KEY: Record<string, string> = {
  openapi: "openapi",
  mcp: "mcp",
  graphql: "graphql",
  googleDiscovery: "google",
};

const normalizeUrl = (url: string | undefined): string | null => {
  if (!url) return null;
  try {
    const parsed = new URL(url);
    parsed.hash = "";
    parsed.searchParams.sort();
    return parsed.toString().replace(/\/$/, "");
  } catch {
    return url.trim().replace(/\/$/, "");
  }
};

const googleApiServiceFromUrl = (url: string | undefined): string | null => {
  if (!url) return null;
  try {
    const parsed = new URL(url);
    const hostname = parsed.hostname.toLowerCase();
    const segments = parsed.pathname.split("/").filter(Boolean);

    if (
      hostname === "www.googleapis.com" &&
      segments[0] === "discovery" &&
      segments[2] === "apis" &&
      segments[3]
    ) {
      return segments[3];
    }

    if (hostname === "www.googleapis.com") return segments[0] ?? null;

    const suffix = ".googleapis.com";
    if (hostname.endsWith(suffix)) {
      const service = hostname.slice(0, -suffix.length);
      return service.length > 0 ? service : null;
    }
  } catch {
    return null;
  }
  return null;
};

const normalizeToken = (value: string | undefined): string =>
  value?.toLowerCase().replace(/[^a-z0-9]+/g, "") ?? "";

const tokenVariants = (value: string | undefined): readonly string[] => {
  const tokens = new Set<string>();
  const normalized = normalizeToken(value);
  if (normalized.length > 0) tokens.add(normalized);

  const parts =
    value
      ?.toLowerCase()
      .split(/[^a-z0-9]+/g)
      .filter(Boolean) ?? [];
  const noise = new Set([
    "api",
    "mcp",
    "openapi",
    "rest",
    "graphql",
    "web",
    "com",
    "net",
    "org",
    "dev",
  ]);
  const cleaned = parts.filter((part) => !noise.has(part));
  const cleanedToken = cleaned.join("");
  if (cleanedToken.length > 0) tokens.add(cleanedToken);
  const aliases: Record<string, string> = {
    pscale: "planetscale",
  };
  for (const part of cleaned) {
    tokens.add(part);
    const alias = aliases[part];
    if (alias) tokens.add(alias);
  }

  return [...tokens];
};

export function integrationInferredUrl(source: {
  readonly id: string;
  readonly name?: string;
}): string | null {
  const hostPattern = /^[a-z0-9][a-z0-9.-]*\.(?:app|co|com|dev|io|net|org)(?:\/.*)?$/i;
  const hostLike = [source.name, source.id.replaceAll("_", ".")]
    .filter((value): value is string => value != null && value.length > 0)
    .find((value) => hostPattern.test(value.trim()));
  if (hostLike) return `https://${hostLike.trim().replace(/^https?:\/\//i, "")}`;

  return null;
}

const tokenMatches = (sourceValue: string, presetValue: string): boolean =>
  presetValue.length > 0 &&
  sourceValue.length > 0 &&
  (sourceValue === presetValue ||
    sourceValue.includes(presetValue) ||
    presetValue.includes(sourceValue));

export function integrationPresetIconUrl(
  source: {
    readonly id: string;
    readonly kind: string;
    readonly name?: string;
    readonly url?: string;
  },
  integrationPlugins: readonly IntegrationPlugin[],
): string | null {
  const pluginKey = KIND_TO_PLUGIN_KEY[source.kind] ?? source.kind;
  const plugin = integrationPlugins.find((p) => p.key === pluginKey);
  const presets = plugin?.presets ?? [];
  const sourceUrl = normalizeUrl(source.url);
  const sourceGoogleService = googleApiServiceFromUrl(source.url);
  const sourceTokens = [...tokenVariants(source.id), ...tokenVariants(source.name)];

  const preset = presets.find((p) => {
    const presetUrl = normalizeUrl(p.url);
    const presetGoogleService = googleApiServiceFromUrl(p.url);
    const presetTokens = [...tokenVariants(p.id), ...tokenVariants(p.name)];
    return (
      (sourceUrl !== null && presetUrl === sourceUrl) ||
      (sourceGoogleService !== null && presetGoogleService === sourceGoogleService) ||
      sourceTokens.some((sourceToken) =>
        presetTokens.some((presetToken) => tokenMatches(sourceToken, presetToken)),
      )
    );
  });

  return preset?.icon ?? null;
}

// Resolution cascade for the rendered favicon: first non-null, non-failed of an
// explicit preset icon, the bundled local icon for a known source id, the
// integrations.sh logo proxy derived from the source URL, then the Google
// favicon service as a last resort. The built-in executor source has no preset
// icon and no URL, so it resolves ONLY through the sourceId branch: callers
// that drop sourceId fall through to the neutral BoxIcon placeholder.
export function integrationFaviconSrc(args: {
  icon?: string | null;
  sourceId?: string;
  url?: string;
  size: number;
  failedSrcs?: readonly string[];
}): string | null {
  const failedSrcs = args.failedSrcs ?? [];
  return (
    [
      args.icon ?? null,
      integrationLocalIconUrl(args.sourceId),
      integrationFaviconUrl(args.url, args.size),
      integrationFaviconFallbackUrl(args.url, args.size),
    ].find((candidate) => candidate !== null && !failedSrcs.includes(candidate)) ?? null
  );
}

export function IntegrationFavicon({
  icon,
  sourceId,
  url,
  size = 16,
}: {
  icon?: string | null;
  sourceId?: string;
  url?: string;
  size?: number;
}) {
  const [failedSrcs, setFailedSrcs] = useState<readonly string[]>([]);
  const src = integrationFaviconSrc({ icon, sourceId, url, size, failedSrcs });

  if (!src) {
    return (
      <BoxIcon
        aria-hidden
        className="shrink-0 text-muted-foreground"
        style={{ width: size, height: size }}
      />
    );
  }

  return (
    <img
      src={src}
      alt=""
      width={size}
      height={size}
      loading="lazy"
      onError={() =>
        setFailedSrcs((current) => (current.includes(src) ? current : [...current, src]))
      }
      className="shrink-0 rounded-sm"
      style={{ width: size, height: size }}
    />
  );
}
