import fs from "fs";
import path from "path";
import { JWT } from "google-auth-library";

const INDEXING_SCOPE = "https://www.googleapis.com/auth/indexing";
const GOOGLE_PUBLISH_ENDPOINT = "https://indexing.googleapis.com/v3/urlNotifications:publish";
const DEFAULT_SITE_URL = "https://nytsolvers.com";

// Bing / IndexNow constants
export const DEFAULT_INDEXNOW_KEY = "82926cf91fb4427eb37acb33bdb2f766";
export const INDEXNOW_ENDPOINT = "https://api.indexnow.org/indexnow";
export const DEFAULT_INDEXNOW_HOST = "nytsolvers.com";

export type IndexingNotificationType = "URL_UPDATED" | "URL_DELETED";

export interface IndexingResult {
  success: boolean;
  url: string;
  type: IndexingNotificationType;
  status?: number;
  data?: any;
  error?: string;
  details?: any;
  skipped?: boolean;
  dryRun?: boolean;
  timestamp: string;
}

export interface IndexNowResult {
  success: boolean;
  provider: "IndexNow (Bing/Yandex)";
  host: string;
  key: string;
  urls: string[];
  status?: number;
  data?: any;
  error?: string;
  dryRun?: boolean;
  timestamp: string;
}

export interface MultiSearchEngineIndexingResult {
  url: string;
  type: IndexingNotificationType;
  google: IndexingResult;
  bing: IndexNowResult;
  timestamp: string;
}

interface ServiceAccountCredentials {
  client_email?: string;
  private_key?: string;
  project_id?: string;
  [key: string]: any;
}

let cachedJwtClient: JWT | null = null;
let cachedCredentialsSource: string | null = null;

/**
 * Returns the configured IndexNow key (env variable or default).
 */
export function getIndexNowKey(): string {
  return process.env.INDEXNOW_KEY || DEFAULT_INDEXNOW_KEY;
}

/**
 * Returns the configured IndexNow host (env variable or default).
 */
export function getIndexNowHost(): string {
  return process.env.INDEXNOW_HOST || DEFAULT_INDEXNOW_HOST;
}

/**
 * Discovers and loads the Google Service Account credentials.
 */
export function loadServiceAccountCredentials(): {
  credentials: ServiceAccountCredentials | null;
  keyPath: string | null;
  source: string;
} {
  // 1. Raw JSON string in environment variable
  if (process.env.GOOGLE_SERVICE_ACCOUNT_JSON) {
    try {
      const parsed = JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_JSON);
      return { credentials: parsed, keyPath: null, source: "env.GOOGLE_SERVICE_ACCOUNT_JSON" };
    } catch (e: any) {
      console.warn("[Google Indexing API] Invalid JSON in GOOGLE_SERVICE_ACCOUNT_JSON:", e.message);
    }
  }

  // 2. Candidate paths
  const candidatePaths = [
    process.env.SERVICE_ACCOUNT_KEY_PATH,
    process.env.GOOGLE_APPLICATION_CREDENTIALS,
    path.resolve(process.cwd(), "service_account.json"),
    path.resolve(process.cwd(), "../service_account.json"),
    path.resolve(__dirname, "../../../service_account.json"),
    path.resolve(__dirname, "../../../../service_account.json"),
    "/var/www/frontend/service_account.json",
    "/var/www/service_account.json",
    path.resolve(process.cwd(), "credential.json"),
    path.resolve(process.cwd(), "../credential.json"),
    path.resolve(__dirname, "../../../credential.json"),
    path.resolve(__dirname, "../../../../credential.json"),
    "/var/www/frontend/credential.json",
    "/var/www/credential.json",
  ].filter(Boolean) as string[];

  for (const filePath of candidatePaths) {
    try {
      const resolved = path.resolve(filePath);
      if (fs.existsSync(resolved)) {
        const raw = fs.readFileSync(resolved, "utf-8");
        const parsed = JSON.parse(raw);
        if (parsed.client_email && parsed.private_key) {
          return { credentials: parsed, keyPath: resolved, source: `file:${resolved}` };
        }
      }
    } catch (err: any) {
      // Continue to next candidate
    }
  }

  return { credentials: null, keyPath: null, source: "none" };
}

/**
 * Returns an authorized JWT client for Google Indexing API.
 */
export function getIndexingJwtClient(): { client: JWT | null; source: string } {
  if (cachedJwtClient && cachedCredentialsSource) {
    return { client: cachedJwtClient, source: cachedCredentialsSource };
  }

  const { credentials, source } = loadServiceAccountCredentials();
  if (!credentials || !credentials.client_email || !credentials.private_key) {
    return { client: null, source };
  }

  try {
    const jwt = new JWT({
      email: credentials.client_email,
      key: credentials.private_key,
      scopes: [INDEXING_SCOPE],
    });

    cachedJwtClient = jwt;
    cachedCredentialsSource = source;
    return { client: jwt, source };
  } catch (error: any) {
    console.error("[Google Indexing API] Error initializing JWT client:", error.message);
    return { client: null, source };
  }
}

/**
 * Builds a standardized canonical URL for a tool, custom page, or blog.
 */
export function buildCanonicalUrl(slug: string, isBlog = false): string {
  const base = (process.env.SITE_URL || DEFAULT_SITE_URL).replace(/\/+$/, "");
  const cleanSlug = slug.trim().replace(/^\/+/, "").replace(/\/+$/, "");

  if (isBlog) {
    return `${base}/blog/${cleanSlug}`;
  }
  if (!cleanSlug) {
    return `${base}/`;
  }
  return `${base}/${cleanSlug}`;
}

/**
 * Pings Google's Indexing API v3 with URL_UPDATED or URL_DELETED.
 * Safe fire-and-forget helper that never throws unhandled errors.
 */
export async function pingGoogleIndexing(
  targetUrl: string,
  type: IndexingNotificationType = "URL_UPDATED"
): Promise<IndexingResult> {
  const timestamp = new Date().toISOString();
  const trimmedUrl = targetUrl ? targetUrl.trim() : "";

  if (!trimmedUrl || (!trimmedUrl.startsWith("http://") && !trimmedUrl.startsWith("https://"))) {
    console.warn(`[Google Indexing API] Invalid URL provided for indexing ping: "${targetUrl}"`);
    return {
      success: false,
      url: trimmedUrl,
      type,
      status: 400,
      error: "Invalid URL: Must start with http:// or https://",
      timestamp,
    };
  }

  const isDryRun = process.env.INDEXING_DRY_RUN === "true";
  if (isDryRun) {
    console.log(`[Google Indexing API] [DRY RUN] Would notify: ${trimmedUrl} (${type})`);
    return {
      success: true,
      url: trimmedUrl,
      type,
      status: 200,
      dryRun: true,
      timestamp,
    };
  }

  const { client, source } = getIndexingJwtClient();

  if (!client) {
    console.warn(
      `[Google Indexing API] Credentials not found (source: ${source}), skipping indexing ping for: ${trimmedUrl}`
    );
    return {
      success: false,
      skipped: true,
      url: trimmedUrl,
      type,
      error: "Google Service Account credentials not found",
      timestamp,
    };
  }

  try {
    const res = await client.request({
      url: GOOGLE_PUBLISH_ENDPOINT,
      method: "POST",
      data: {
        url: trimmedUrl,
        type,
      },
    });

    console.log(
      `[Google Indexing API] ✔ SUCCESS: ${trimmedUrl} (${type}) -> HTTP ${res.status}`
    );

    return {
      success: true,
      url: trimmedUrl,
      type,
      status: res.status,
      data: res.data,
      timestamp,
    };
  } catch (error: any) {
    const statusCode = error.status || error.code || error.response?.status || 500;
    const errorMsg = error.response?.data?.error?.message || error.message || "Unknown error";
    const details = error.response?.data?.error || null;

    console.error(
      `[Google Indexing API] ✖ ERROR pinging ${trimmedUrl} (${type}) -> HTTP ${statusCode}: ${errorMsg}`
    );

    return {
      success: false,
      url: trimmedUrl,
      type,
      status: typeof statusCode === "number" ? statusCode : 500,
      error: errorMsg,
      details,
      timestamp,
    };
  }
}

/**
 * Pings Bing IndexNow API (supporting Microsoft Bing, Yandex, Naver, and Seznam).
 * Safe fire-and-forget helper that never throws unhandled errors.
 */
export async function pingBingIndexNow(
  targetUrls: string | string[]
): Promise<IndexNowResult> {
  const timestamp = new Date().toISOString();
  const urlList = (Array.isArray(targetUrls) ? targetUrls : [targetUrls])
    .map((u) => (typeof u === "string" ? u.trim() : ""))
    .filter((u) => u.startsWith("http://") || u.startsWith("https://"));

  const host = getIndexNowHost();
  const key = getIndexNowKey();
  const keyLocation = process.env.INDEXNOW_KEY_LOCATION || `https://${host}/${key}.txt`;

  if (urlList.length === 0) {
    console.warn(`[Bing IndexNow] No valid URLs provided for IndexNow submission`);
    return {
      success: false,
      provider: "IndexNow (Bing/Yandex)",
      host,
      key,
      urls: [],
      status: 400,
      error: "No valid URLs provided",
      timestamp,
    };
  }

  const isDryRun = process.env.INDEXING_DRY_RUN === "true";
  if (isDryRun) {
    console.log(
      `[Bing IndexNow] [DRY RUN] Would submit ${urlList.length} URL(s) to IndexNow (${host}, key: ${key})`
    );
    return {
      success: true,
      provider: "IndexNow (Bing/Yandex)",
      host,
      key,
      urls: urlList,
      status: 200,
      dryRun: true,
      timestamp,
    };
  }

  const payload = {
    host,
    key,
    keyLocation,
    urlList,
  };

  try {
    const response = await fetch(INDEXNOW_ENDPOINT, {
      method: "POST",
      headers: {
        "Content-Type": "application/json; charset=utf-8",
      },
      body: JSON.stringify(payload),
    });

    const isSuccess = response.status === 200 || response.status === 202;
    const responseText = await response.text();

    if (isSuccess) {
      console.log(
        `[Bing IndexNow] ✔ SUCCESS: Submitted ${urlList.length} URL(s) -> HTTP ${response.status} (${response.statusText || "OK/Accepted"})`
      );
      return {
        success: true,
        provider: "IndexNow (Bing/Yandex)",
        host,
        key,
        urls: urlList,
        status: response.status,
        data: responseText || "Accepted",
        timestamp,
      };
    } else {
      console.error(
        `[Bing IndexNow] ✖ ERROR: HTTP ${response.status} (${response.statusText}): ${responseText}`
      );
      return {
        success: false,
        provider: "IndexNow (Bing/Yandex)",
        host,
        key,
        urls: urlList,
        status: response.status,
        error: responseText || response.statusText,
        timestamp,
      };
    }
  } catch (error: any) {
    console.error(`[Bing IndexNow] ✖ Network error pinging IndexNow: ${error.message}`);
    return {
      success: false,
      provider: "IndexNow (Bing/Yandex)",
      host,
      key,
      urls: urlList,
      status: 500,
      error: error.message,
      timestamp,
    };
  }
}

/**
 * Universal helper to ping page changes to BOTH Google Indexing API and Bing IndexNow simultaneously.
 */
export async function pingPageChange(
  slug: string,
  type: IndexingNotificationType = "URL_UPDATED",
  isBlog = false
): Promise<MultiSearchEngineIndexingResult> {
  const timestamp = new Date().toISOString();
  const url = buildCanonicalUrl(slug, isBlog);

  // Dispatch both Google and Bing notifications in parallel
  const [googleSettled, bingSettled] = await Promise.allSettled([
    pingGoogleIndexing(url, type),
    pingBingIndexNow(url),
  ]);

  const google: IndexingResult =
    googleSettled.status === "fulfilled"
      ? googleSettled.value
      : {
          success: false,
          url,
          type,
          status: 500,
          error: googleSettled.reason?.message || "Unknown error",
          timestamp,
        };

  const bing: IndexNowResult =
    bingSettled.status === "fulfilled"
      ? bingSettled.value
      : {
          success: false,
          provider: "IndexNow (Bing/Yandex)",
          host: getIndexNowHost(),
          key: getIndexNowKey(),
          urls: [url],
          status: 500,
          error: bingSettled.reason?.message || "Unknown error",
          timestamp,
        };

  return {
    url,
    type,
    google,
    bing,
    timestamp,
  };
}

export const DAILY_PUZZLE_PAGES: Array<{ slug: string; name: string }> = [
  { slug: "connections-hints", name: "Connections Hints" },
  { slug: "wordle-hints", name: "Wordle Hints" },
  { slug: "strands-hints", name: "Strands Hints" },
  { slug: "spelling-bee-hints", name: "Spelling Bee Hints" },
  { slug: "letter-boxed-hints", name: "Letter Boxed Hints" },
  { slug: "mini-crossword-hints", name: "The Mini Crossword Hints" },
  { slug: "crossword-hints", name: "Daily Crossword Hints" },
  { slug: "connections-sports-hints", name: "Sports Connections Hints" },
  { slug: "pips-hints", name: "Pips Hints" },
  { slug: "betweenle-hints", name: "Betweenle Hints" },
  { slug: "conexo-hints", name: "Conexo Hints" },
  { slug: "blossom-hints", name: "Blossom Hints" },
  { slug: "quordle-hints", name: "Quordle Hints" },
  { slug: "la-times-mini-hints", name: "LA Times Mini Crossword Hints" },
  { slug: "la-times-crossword-hints", name: "LA Times Crossword Hints" },
  { slug: "word-salad-hints", name: "Word Salad Hints" },
  { slug: "daily-answers", name: "NYT Solvers Daily Answers" },
];

/**
 * Returns current date string (YYYY-MM-DD) in America/New_York timezone
 */
export function getTodayNewYorkDate(): string {
  try {
    return new Intl.DateTimeFormat("en-CA", {
      timeZone: "America/New_York",
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
    }).format(new Date());
  } catch {
    return new Date().toISOString().split("T")[0];
  }
}

// In-memory state fallback
const inMemoryDailyState: Record<string, string> = {};

function getDailyIndexingStateFilePath(): string {
  const candidateDirs = [
    path.resolve(process.cwd(), "cache"),
    path.resolve(process.cwd(), "backend/cache"),
    path.resolve(__dirname, "../../cache"),
    path.resolve(__dirname, "../../../cache"),
    "/var/www/frontend/cache",
  ];
  for (const dir of candidateDirs) {
    try {
      if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
      }
      return path.join(dir, "daily-indexing-state.json");
    } catch {
      // try next
    }
  }
  return path.resolve(process.cwd(), "daily-indexing-state.json");
}

export function getDailyIndexingState(): Record<string, string> {
  const filePath = getDailyIndexingStateFilePath();
  try {
    if (fs.existsSync(filePath)) {
      const fileData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
      return { ...inMemoryDailyState, ...fileData };
    }
  } catch (err: any) {
    console.warn("[Daily Indexing] Could not read daily state file:", err.message);
  }
  return { ...inMemoryDailyState };
}

export function saveDailyIndexingState(state: Record<string, string>): void {
  Object.assign(inMemoryDailyState, state);
  const filePath = getDailyIndexingStateFilePath();
  try {
    const parentDir = path.dirname(filePath);
    if (!fs.existsSync(parentDir)) {
      fs.mkdirSync(parentDir, { recursive: true });
    }
    fs.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
  } catch (err: any) {
    console.warn("[Daily Indexing] Could not save daily state file:", err.message);
  }
}

export interface DailyPuzzlePingResult {
  skipped: boolean;
  slug: string;
  dateStr: string;
  result?: MultiSearchEngineIndexingResult;
  reason?: string;
}

/**
 * Pings Google and Bing when a daily puzzle hint file is generated or updated.
 * Automatically dedupes by (slug + dateStr) so that hourly cron runs will ONLY ping once per day!
 */
export async function pingDailyPuzzleUpdated(
  slug: string,
  dateStr?: string,
  options?: { force?: boolean }
): Promise<DailyPuzzlePingResult> {
  const targetDate = dateStr || getTodayNewYorkDate();
  const state = getDailyIndexingState();

  if (!options?.force && state[slug] === targetDate) {
    console.log(
      `[Daily Indexing] "${slug}" for date ${targetDate} already pinged search engines today; skipping duplicate.`
    );
    return {
      skipped: true,
      slug,
      dateStr: targetDate,
      reason: "already_pinged_today",
    };
  }

  console.log(
    `[Daily Indexing] Pinging Google & Bing for "${slug}" (date: ${targetDate})...`
  );

  const result = await pingPageChange(slug, "URL_UPDATED");

  // If at least one search engine accepted or dry-run, mark as pinged
  if (
    result.google.success ||
    result.bing.success ||
    result.google.dryRun ||
    result.bing.dryRun
  ) {
    state[slug] = targetDate;
    saveDailyIndexingState(state);
  }

  return {
    skipped: false,
    slug,
    dateStr: targetDate,
    result,
  };
}

export interface AllDailyPuzzlePingSummary {
  total: number;
  pinged: number;
  skipped: number;
  succeeded: number;
  failed: number;
  results: Record<string, any>;
}

/**
 * Pings all daily puzzle pages + homepage simultaneously to Bing IndexNow (batch)
 * and Google Indexing API (throttled) at midnight New York time.
 */
export async function pingAllDailyPuzzlePages(
  options?: { force?: boolean }
): Promise<AllDailyPuzzlePingSummary> {
  const targetDate = getTodayNewYorkDate();
  const state = getDailyIndexingState();

  const allSlugs = [
    "", // Homepage
    ...DAILY_PUZZLE_PAGES.map((p) => p.slug),
  ];

  const candidateSlugs = allSlugs.filter((slug) => {
    if (options?.force) return true;
    const key = slug === "" ? "__homepage__" : slug;
    return state[key] !== targetDate;
  });

  if (candidateSlugs.length === 0) {
    console.log(
      `[Daily Indexing] All ${allSlugs.length} daily puzzle pages are already up-to-date for ${targetDate}.`
    );
    return {
      total: allSlugs.length,
      pinged: 0,
      skipped: allSlugs.length,
      succeeded: 0,
      failed: 0,
      results: {},
    };
  }

  console.log(
    `[Daily Indexing] Starting batch daily indexing ping for ${candidateSlugs.length} pages (date: ${targetDate})...`
  );

  const candidateUrls = candidateSlugs.map((slug) => buildCanonicalUrl(slug));

  // 1. Bing IndexNow: batch submit all URLs in 1 single HTTP request
  const bingPromise = pingBingIndexNow(candidateUrls);

  // 2. Google Indexing: submit sequentially with 500ms delay between calls
  const results: Record<string, any> = {};
  let succeeded = 0;
  let failed = 0;

  const bingResult = await bingPromise;

  for (let i = 0; i < candidateSlugs.length; i++) {
    const slug = candidateSlugs[i];
    const url = candidateUrls[i];
    const key = slug === "" ? "__homepage__" : slug;

    const googleResult = await pingGoogleIndexing(url, "URL_UPDATED");

    const isOk =
      googleResult.success ||
      googleResult.dryRun ||
      bingResult.success ||
      bingResult.dryRun;

    if (isOk) {
      state[key] = targetDate;
      succeeded++;
    } else {
      failed++;
    }

    results[key] = {
      url,
      google: googleResult,
      bing: {
        success: bingResult.success,
        status: bingResult.status,
      },
    };

    if (i < candidateSlugs.length - 1) {
      await new Promise((r) => setTimeout(r, 500));
    }
  }

  saveDailyIndexingState(state);

  return {
    total: allSlugs.length,
    pinged: candidateSlugs.length,
    skipped: allSlugs.length - candidateSlugs.length,
    succeeded,
    failed,
    results,
  };
}
