import {
  parseSearchCitiesSetting,
  serializeSearchCitiesSetting,
} from "@shared/search-cities.js";
import type { UpdateSettingsInput } from "@shared/settings-schema.js";
import type { JobSource } from "@shared/types";

/**
 * Same merge order as server discovery (`discover-jobs`): explicit search terms
 * first, then profile target roles that are not already present (case-insensitive).
 */
export function mergeDiscoverySearchTerms(
  savedTerms: string[],
  profileTargetRoles: string[] | null | undefined,
): string[] {
  const normalized = savedTerms.map((t) => t.trim()).filter(Boolean);
  const existingLower = new Set(normalized.map((t) => t.toLowerCase()));
  const out = [...normalized];
  for (const role of profileTargetRoles ?? []) {
    if (typeof role !== "string") continue;
    const trimmed = role.trim();
    if (!trimmed) continue;
    const key = trimmed.toLowerCase();
    if (existingLower.has(key)) continue;
    out.push(trimmed);
    existingLower.add(key);
  }
  return out;
}

export type AutomaticPresetId = "fast" | "balanced" | "detailed";
export type WorkplaceType = "remote" | "hybrid" | "onsite";
export const WORKPLACE_TYPE_OPTIONS: WorkplaceType[] = [
  "remote",
  "hybrid",
  "onsite",
];

export interface AutomaticRunValues {
  topN: number;
  minSuitabilityScore: number;
  searchTerms: string[];
  activeKeywordSetId: string | null;
  runBudget: number;
  country: string;
  cityLocations: string[];
  workplaceTypes: WorkplaceType[];
}

export interface AutomaticPresetValues {
  topN: number;
  minSuitabilityScore: number;
  runBudget: number;
}

export interface AutomaticEstimate {
  discovered: {
    min: number;
    max: number;
    cap: number;
  };
  processed: {
    min: number;
    max: number;
  };
}

export const AUTOMATIC_PRESETS: Record<
  AutomaticPresetId,
  AutomaticPresetValues
> = {
  fast: {
    topN: 5,
    minSuitabilityScore: 75,
    runBudget: 300,
  },
  balanced: {
    topN: 10,
    minSuitabilityScore: 50,
    runBudget: 500,
  },
  detailed: {
    topN: 20,
    minSuitabilityScore: 35,
    runBudget: 1000,
  },
};

export const RUN_MEMORY_STORAGE_KEY = "jobops.pipeline.run-memory.v1";

export interface AutomaticRunMemory {
  topN: number;
  minSuitabilityScore: number;
}

export function normalizeWorkplaceTypes(
  workplaceTypes: WorkplaceType[] | null | undefined,
): WorkplaceType[] {
  const seen = new Set<WorkplaceType>();
  const out: WorkplaceType[] = [];

  for (const workplaceType of workplaceTypes ?? []) {
    if (!WORKPLACE_TYPE_OPTIONS.includes(workplaceType)) continue;
    if (seen.has(workplaceType)) continue;
    seen.add(workplaceType);
    out.push(workplaceType);
  }

  return out.length > 0 ? out : [...WORKPLACE_TYPE_OPTIONS];
}

export interface ExtractorLimits {
  jobspyResultsWanted: number;
  gradcrackerMaxJobsPerTerm: number;
  ukvisajobsMaxJobs: number;
  adzunaMaxJobsPerTerm: number;
  startupjobsMaxJobsPerTerm: number;
}

export function deriveExtractorLimits(args: {
  budget: number;
  searchTerms: string[];
  sources: JobSource[];
}): ExtractorLimits {
  const budget = Math.max(1, Math.round(args.budget));
  const termCount = Math.max(1, args.searchTerms.length);
  const includesIndeed = args.sources.includes("indeed");
  const includesLinkedIn = args.sources.includes("linkedin");
  const includesGlassdoor = args.sources.includes("glassdoor");
  const includesGradcracker = args.sources.includes("gradcracker");
  const includesUkVisaJobs = args.sources.includes("ukvisajobs");
  const includesAdzuna = args.sources.includes("adzuna");
  const includesHiringCafe = args.sources.includes("hiringcafe");
  const includesStartupJobs = args.sources.includes("startupjobs");

  const weightedContributors =
    (includesIndeed ? termCount : 0) +
    (includesLinkedIn ? termCount : 0) +
    (includesGlassdoor ? termCount : 0) +
    (includesGradcracker ? termCount : 0) +
    (includesUkVisaJobs ? 1 : 0) +
    (includesAdzuna ? termCount : 0) +
    (includesHiringCafe ? termCount : 0) +
    (includesStartupJobs ? termCount : 0);

  if (weightedContributors <= 0) {
    return {
      jobspyResultsWanted: budget,
      gradcrackerMaxJobsPerTerm: budget,
      ukvisajobsMaxJobs: budget,
      adzunaMaxJobsPerTerm: budget,
      startupjobsMaxJobsPerTerm: budget,
    };
  }

  const perUnit = Math.max(1, Math.floor(budget / weightedContributors));
  const remainder = Math.max(0, budget - perUnit * weightedContributors);

  return {
    jobspyResultsWanted: perUnit,
    gradcrackerMaxJobsPerTerm: perUnit,
    ukvisajobsMaxJobs: Math.min(budget, perUnit + remainder),
    adzunaMaxJobsPerTerm: perUnit,
    startupjobsMaxJobsPerTerm: perUnit,
  };
}

/** Raise per-source caps before a pipeline run for maximum discovery coverage. */
export function buildMaxCoverageDiscoveryLimits(args: {
  budget: number;
  searchTerms: string[];
  sources: JobSource[];
}): Partial<UpdateSettingsInput> {
  const limits = deriveExtractorLimits(args);
  const perTermCap = Math.max(100, limits.jobspyResultsWanted);
  const boardCap = Math.max(150, perTermCap);

  return {
    jobspyResultsWanted: perTermCap,
    gradcrackerMaxJobsPerTerm: boardCap,
    ukvisajobsMaxJobs: boardCap,
    adzunaMaxJobsPerTerm: boardCap,
    startupjobsMaxJobsPerTerm: boardCap,
    usajobsMaxJobsPerTerm: boardCap,
    jobicyMaxJobsPerTerm: boardCap,
    themuseMaxJobsPerTerm: boardCap,
    joobleMaxJobsPerTerm: boardCap,
    careerjetMaxJobsPerTerm: boardCap,
    reedMaxJobsPerTerm: boardCap,
    remoteokMaxJobsPerTerm: boardCap,
    remotiveMaxJobsPerTerm: boardCap,
    arbeitnowMaxJobsPerTerm: boardCap,
    himalayasMaxJobsPerTerm: boardCap,
    weworkremotelyMaxJobsPerTerm: boardCap,
    workingnomadsMaxJobsPerTerm: boardCap,
    fourdayweekMaxJobsPerTerm: boardCap,
    testdevjobsMaxJobsPerTerm: boardCap,
    testdevjobsMaxPages: 10,
    builtinMaxJobsPerTerm: boardCap,
    builtinMaxPagesPerTerm: 10,
    wellfoundMaxJobsPerTerm: Math.min(100, boardCap),
    googleJobsMaxJobsPerTerm: Math.min(30, boardCap),
    qajobsboardMaxJobsPerTerm: boardCap,
    arcMaxJobsPerPath: boardCap,
    smartrecruitersMaxJobsPerCompany: boardCap,
    elutaMaxJobsPerTerm: boardCap,
    bctenetMaxJobsPerTerm: 400,
    icimsMaxJobsPerTenant: 250,
    icimsMaxPagesPerSearch: 10,
  };
}

export function parseSearchTermsInput(input: string): string[] {
  return input
    .split(/[\n,]/g)
    .map((value) => value.trim())
    .filter(Boolean);
}

export function parseCityLocationsInput(input: string): string[] {
  const parsed = parseSearchTermsInput(input);
  const seen = new Set<string>();
  const out: string[] = [];
  for (const city of parsed) {
    const key = city.toLowerCase();
    if (seen.has(key)) continue;
    seen.add(key);
    out.push(city);
  }
  return out;
}

export function parseCityLocationsSetting(
  location: string | null | undefined,
): string[] {
  return parseSearchCitiesSetting(location);
}

export function serializeCityLocationsSetting(cities: string[]): string | null {
  return serializeSearchCitiesSetting(cities);
}

export function stringifySearchTerms(terms: string[]): string {
  return terms.join("\n");
}

export function calculateAutomaticEstimate(args: {
  values: AutomaticRunValues;
  sources: JobSource[];
}): AutomaticEstimate {
  const { values, sources } = args;
  if (values.searchTerms.length === 0) {
    return {
      discovered: {
        min: 0,
        max: 0,
        cap: 0,
      },
      processed: {
        min: 0,
        max: 0,
      },
    };
  }

  const termCount = values.searchTerms.length;
  const hasGradcracker = sources.includes("gradcracker");
  const hasUkVisaJobs = sources.includes("ukvisajobs");
  const hasIndeed = sources.includes("indeed");
  const hasLinkedIn = sources.includes("linkedin");
  const hasGlassdoor = sources.includes("glassdoor");
  const hasAdzuna = sources.includes("adzuna");
  const hasHiringCafe = sources.includes("hiringcafe");
  const hasStartupJobs = sources.includes("startupjobs");
  const limits = deriveExtractorLimits({
    budget: values.runBudget,
    searchTerms: values.searchTerms,
    sources,
  });

  const jobspySitesCount = [hasIndeed, hasLinkedIn, hasGlassdoor].filter(
    Boolean,
  ).length;
  const jobspyCap = jobspySitesCount * limits.jobspyResultsWanted * termCount;
  const gradcrackerCap = hasGradcracker
    ? limits.gradcrackerMaxJobsPerTerm * termCount
    : 0;
  const ukvisaCap = hasUkVisaJobs ? limits.ukvisajobsMaxJobs : 0;
  const adzunaCap = hasAdzuna ? limits.adzunaMaxJobsPerTerm * termCount : 0;
  const hiringCafeCap = hasHiringCafe
    ? limits.jobspyResultsWanted * termCount
    : 0;
  const startupJobsCap = hasStartupJobs
    ? limits.startupjobsMaxJobsPerTerm * termCount
    : 0;

  const discoveredCap =
    jobspyCap +
    gradcrackerCap +
    ukvisaCap +
    adzunaCap +
    hiringCafeCap +
    startupJobsCap;
  const discoveredMin = Math.round(discoveredCap * 0.35);
  const discoveredMax = Math.round(discoveredCap * 0.75);
  const processedMin = Math.min(values.topN, discoveredMin);
  const processedMax = Math.min(values.topN, discoveredMax);

  return {
    discovered: {
      min: discoveredMin,
      max: discoveredMax,
      cap: discoveredCap,
    },
    processed: {
      min: processedMin,
      max: processedMax,
    },
  };
}

export function loadAutomaticRunMemory(): AutomaticRunMemory | null {
  try {
    const raw = localStorage.getItem(RUN_MEMORY_STORAGE_KEY);
    if (!raw) return null;
    const parsed = JSON.parse(raw) as Partial<AutomaticRunMemory>;
    if (
      typeof parsed.topN !== "number" ||
      typeof parsed.minSuitabilityScore !== "number"
    ) {
      return null;
    }
    return {
      topN: Math.min(50, Math.max(1, Math.round(parsed.topN))),
      minSuitabilityScore: Math.min(
        100,
        Math.max(0, Math.round(parsed.minSuitabilityScore)),
      ),
    };
  } catch {
    return null;
  }
}

export function saveAutomaticRunMemory(memory: AutomaticRunMemory): void {
  try {
    localStorage.setItem(RUN_MEMORY_STORAGE_KEY, JSON.stringify(memory));
  } catch {
    // Ignore localStorage failures
  }
}
