import { spawn, spawnSync } from "node:child_process";
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { envForExtractorSubprocess } from "@shared/extractor-subprocess-env.js";
import type { CreateJobInput } from "@shared/types/jobs";

const srcDir = dirname(fileURLToPath(import.meta.url));
const EXTRACTOR_DIR = join(srcDir, "..");
const OUTPUT_PATH = join(EXTRACTOR_DIR, "storage/jobs.json");
const require = createRequire(import.meta.url);
const TSX_CLI_PATH = (() => {
  try {
    return require.resolve("tsx/dist/cli.mjs");
  } catch {
    return null;
  }
})();

/** Browser scrape can take a while (per-term navigation + per-card clicks). */
const SUBPROCESS_TIMEOUT_MS = 4 * 60 * 1000;

interface ScrapedJob {
  title?: string;
  employer?: string;
  jobUrl?: string;
  applicationLink?: string;
  location?: string;
  jobDescription?: string;
  postedVia?: string;
  isRemote?: boolean;
}

export interface RunGoogleJobsOptions {
  searchTerms?: string[];
  maxJobs?: number;
}

export interface GoogleJobsResult {
  success: boolean;
  jobs: CreateJobInput[];
  error?: string;
}

function canRunNpmCommand(): boolean {
  const result = spawnSync("npm", ["--version"], { stdio: "ignore" });
  return !result.error && result.status === 0;
}

function mapJob(row: ScrapedJob): CreateJobInput | null {
  const jobUrl = row.jobUrl?.trim();
  if (!jobUrl) return null;
  return {
    source: "google-jobs",
    title: row.title?.trim() || "Unknown Title",
    employer: row.employer?.trim() || "Unknown Employer",
    jobUrl,
    applicationLink: row.applicationLink?.trim() || jobUrl,
    location: row.location?.trim() || undefined,
    jobDescription: row.jobDescription?.trim() || undefined,
    isRemote: row.isRemote ?? undefined,
  };
}

export async function runGoogleJobs(
  options: RunGoogleJobsOptions = {},
): Promise<GoogleJobsResult> {
  const searchTerms =
    options.searchTerms && options.searchTerms.length > 0
      ? options.searchTerms
      : ["software engineer"];
  const maxJobs = options.maxJobs ?? 30;

  const useNpmCommand = canRunNpmCommand();
  if (!TSX_CLI_PATH && !useNpmCommand) {
    return {
      success: false,
      jobs: [],
      error: "Unable to execute Google Jobs extractor (npm/tsx unavailable)",
    };
  }

  try {
    await new Promise<void>((resolve, reject) => {
      const extractorEnv = envForExtractorSubprocess({
        ...process.env,
        GOOGLE_JOBS_SEARCH_TERMS: JSON.stringify(searchTerms),
        GOOGLE_JOBS_MAX_JOBS: String(maxJobs),
      });

      const child = TSX_CLI_PATH
        ? spawn(process.execPath, [TSX_CLI_PATH, join(srcDir, "main.ts")], {
            cwd: EXTRACTOR_DIR,
            env: extractorEnv,
            stdio: ["ignore", "pipe", "pipe"],
          })
        : spawn("npx", ["tsx", join(srcDir, "main.ts")], {
            cwd: EXTRACTOR_DIR,
            env: extractorEnv,
            stdio: ["ignore", "pipe", "pipe"],
          });

      let stderr = "";
      child.stderr?.on("data", (chunk: Buffer) => {
        stderr += chunk.toString();
      });

      const timeout = setTimeout(() => {
        child.kill("SIGKILL");
        reject(new Error("Google Jobs browser scrape timed out"));
      }, SUBPROCESS_TIMEOUT_MS);

      child.on("error", (error) => {
        clearTimeout(timeout);
        reject(error);
      });
      child.on("close", (code) => {
        clearTimeout(timeout);
        if (code === 0) {
          resolve();
          return;
        }
        reject(
          new Error(
            stderr.trim() ||
              `Google Jobs browser scrape exited with code ${code}`,
          ),
        );
      });
    });

    const raw = await readFile(OUTPUT_PATH, "utf-8");
    const parsed = JSON.parse(raw) as unknown;
    const rows = Array.isArray(parsed) ? parsed : [];

    const jobs: CreateJobInput[] = [];
    const seen = new Set<string>();
    for (const value of rows) {
      if (!value || typeof value !== "object" || Array.isArray(value)) continue;
      const mapped = mapJob(value as ScrapedJob);
      if (!mapped) continue;
      const key = mapped.jobUrl;
      if (seen.has(key)) continue;
      seen.add(key);
      jobs.push(mapped);
    }

    const { filterExpiredJobListings } = await import(
      "@shared/job-listing-probe.js"
    );
    const { kept, dropped } = await filterExpiredJobListings(jobs, {
      concurrency: 4,
      timeoutMs: 5_000,
    });

    if (dropped > 0) {
      console.info(
        `[google-jobs] Dropped ${dropped} expired listing(s) after URL probe`,
      );
    }

    return { success: true, jobs: kept };
  } catch (error) {
    const message = error instanceof Error ? error.message : "Unknown error";
    return { success: false, jobs: [], error: message };
  }
}
