import { z } from "zod";
import {
  CHAT_STYLE_LANGUAGE_MODE_VALUES,
  CHAT_STYLE_MANUAL_LANGUAGE_VALUES,
  type ChatStyleLanguageMode,
  type ChatStyleManualLanguage,
  type JobSearchProfile,
  type ResumeProjectsSettings,
} from "./types/settings";

function parseNonEmptyStringOrNull(raw: string | undefined): string | null {
  return raw === undefined || raw === "" ? null : raw;
}

function parseIntOrNull(raw: string | undefined): number | null {
  if (!raw) return null;
  const parsed = parseInt(raw, 10);
  return Number.isNaN(parsed) ? null : parsed;
}

function parseJsonArrayOrNull(raw: string | undefined): string[] | null {
  if (!raw) return null;
  try {
    const parsed = JSON.parse(raw);
    return Array.isArray(parsed) ? (parsed as string[]) : null;
  } catch {
    return null;
  }
}

/**
 * Parse a delimited list (comma / newline / pipe) into a deduped, trimmed
 * array. Used for env-backed defaults like LEVER_COMPANIES="acme,stripe".
 */
function parseCompanyList(raw: string | undefined | null): string[] {
  if (!raw) return [];
  const out: string[] = [];
  const seen = new Set<string>();
  for (const piece of raw.split(/[\n,;|]+/)) {
    const value = piece.trim();
    if (!value) continue;
    if (seen.has(value)) continue;
    seen.add(value);
    out.push(value);
  }
  return out;
}

function parseBitBoolOrNull(raw: string | undefined): boolean | null {
  if (!raw) return null;
  return raw === "true" || raw === "1";
}

function normalizeLlmProviderOrNull(raw: string | undefined): string | null {
  if (raw === undefined) return null;
  const normalized = raw.trim().toLowerCase().replace(/-/g, "_");
  return normalized ? normalized : null;
}

export const DEFAULT_GEMINI_MODEL = "google/gemini-3-flash-preview";
export const DEFAULT_OPENAI_MODEL = "gpt-5.4-mini";

export function getDefaultModelForProvider(
  provider: string | null | undefined,
  fallbackModel?: string | null,
): string {
  const trimmedFallback = fallbackModel?.trim();
  if (trimmedFallback) {
    return trimmedFallback;
  }

  const normalizedProvider = normalizeLlmProviderOrNull(provider ?? undefined);

  if (normalizedProvider === "openai") {
    return DEFAULT_OPENAI_MODEL;
  }

  if (normalizedProvider === "gemini") {
    return DEFAULT_GEMINI_MODEL;
  }
  return DEFAULT_GEMINI_MODEL;
}

function serializeNullableNumber(
  value: number | null | undefined,
): string | null {
  return value !== null && value !== undefined ? String(value) : null;
}

function serializeNullableJsonArray(
  value: string[] | null | undefined,
): string | null {
  return value !== null && value !== undefined ? JSON.stringify(value) : null;
}

function serializeBitBool(value: boolean | null | undefined): string | null {
  if (value === null || value === undefined) return null;
  return value ? "1" : "0";
}

function createEnumParser<const TValues extends readonly [string, ...string[]]>(
  values: TValues,
): (raw: string | undefined) => TValues[number] | null {
  const allowedValues = new Set<string>(values);

  return (raw: string | undefined): TValues[number] | null => {
    if (!raw) return null;
    return allowedValues.has(raw) ? (raw as TValues[number]) : null;
  };
}

function createEnumArrayParser<
  const TValues extends readonly [string, ...string[]],
>(values: TValues): (raw: string | undefined) => TValues[number][] | null {
  const allowedValues = new Set<string>(values);

  return (raw: string | undefined): TValues[number][] | null => {
    if (!raw) return null;
    try {
      const parsed = JSON.parse(raw) as unknown;
      if (!Array.isArray(parsed)) return null;

      const out: TValues[number][] = [];
      const seen = new Set<string>();
      for (const value of parsed) {
        if (typeof value !== "string" || !allowedValues.has(value)) {
          return null;
        }
        if (seen.has(value)) continue;
        seen.add(value);
        out.push(value as TValues[number]);
      }
      if (out.length === 0) return null;
      return out;
    } catch {
      return null;
    }
  };
}

const parseChatStyleLanguageModeOrNull = createEnumParser(
  CHAT_STYLE_LANGUAGE_MODE_VALUES,
);

const parseChatStyleManualLanguageOrNull = createEnumParser(
  CHAT_STYLE_MANUAL_LANGUAGE_VALUES,
);

const WORKPLACE_TYPE_VALUES = ["remote", "hybrid", "onsite"] as const;
const parseWorkplaceTypesOrNull = createEnumArrayParser(WORKPLACE_TYPE_VALUES);

export const jobSearchProfileSchema = z.object({
  targetRoles: z.array(z.string().trim().min(1).max(200)).max(20),
  experienceLevel: z.string().trim().max(50),
  mustHaveSkills: z.array(z.string().trim().min(1).max(200)).max(50),
  niceToHaveSkills: z.array(z.string().trim().min(1).max(200)).max(50),
  dealBreakers: z.array(z.string().trim().min(1).max(200)).max(50),
  preferredWorkArrangement: z.array(z.string().trim().min(1).max(50)).max(5),
  preferredLocations: z.array(z.string().trim().min(1).max(200)).max(20),
  minimumSalary: z.string().trim().max(100),
  industriesToTarget: z.array(z.string().trim().min(1).max(200)).max(20),
  industriesToAvoid: z.array(z.string().trim().min(1).max(200)).max(20),
  aboutMe: z.string().trim().max(4000),
  basicAuthUser: z.string().trim().max(100).nullable().optional(),
  resumeLocalPath: z.string().trim().max(500).nullable().optional(),
});

export const resumeProjectsSchema = z.object({
  maxProjects: z.number().int().min(0).max(100),
  lockedProjectIds: z.array(z.string().trim().min(1)).max(200),
  aiSelectableProjectIds: z.array(z.string().trim().min(1)).max(200),
});

const DEFAULT_JOB_SEARCH_PROFILE: JobSearchProfile = {
  targetRoles: [],
  experienceLevel: "",
  mustHaveSkills: [],
  niceToHaveSkills: [],
  dealBreakers: [],
  preferredWorkArrangement: [],
  preferredLocations: [],
  minimumSalary: "",
  industriesToTarget: [],
  industriesToAvoid: [],
  aboutMe: "",
};

export const settingsRegistry = {
  // --- Typed Settings ---
  jobSearchProfile: {
    kind: "typed" as const,
    schema: jobSearchProfileSchema,
    default: (): JobSearchProfile => DEFAULT_JOB_SEARCH_PROFILE,
    parse: (raw: string | undefined): JobSearchProfile | null => {
      if (!raw) return null;
      try {
        return JSON.parse(raw);
      } catch {
        return null;
      }
    },
    serialize: (value: JobSearchProfile | null | undefined): string | null => {
      return value ? JSON.stringify(value) : null;
    },
  },
  model: {
    kind: "typed" as const,
    schema: z.string().trim().max(200),
    default: (): string =>
      typeof process !== "undefined"
        ? getDefaultModelForProvider(
            process.env.LLM_PROVIDER,
            process.env.MODEL,
          )
        : DEFAULT_GEMINI_MODEL,
    parse: parseNonEmptyStringOrNull,
    serialize: (value: string | null | undefined): string | null =>
      value ?? null,
  },
  llmProvider: {
    kind: "typed" as const,
    envKey: "LLM_PROVIDER",
    schema: z.preprocess(
      (v) => (typeof v === "string" ? normalizeLlmProviderOrNull(v) : v),
      z
        .enum([
          "openrouter",
          "lmstudio",
          "ollama",
          "openai",
          "openai_compatible",
          "gemini",
        ])
        .nullable(),
    ),
    default: (): string =>
      typeof process !== "undefined"
        ? normalizeLlmProviderOrNull(process.env.LLM_PROVIDER) || "openrouter"
        : "openrouter",
    parse: normalizeLlmProviderOrNull,
    serialize: (value: string | null | undefined): string | null =>
      value ?? null,
  },
  llmBaseUrl: {
    kind: "typed" as const,
    envKey: "LLM_BASE_URL",
    schema: z.preprocess(
      (v) => (v === "" ? null : v),
      z.string().trim().url().max(2000).nullable(),
    ),
    default: (): string =>
      typeof process !== "undefined" ? process.env.LLM_BASE_URL || "" : "",
    parse: parseNonEmptyStringOrNull,
    serialize: (value: string | null | undefined): string | null =>
      value ?? null,
  },
  pipelineWebhookUrl: {
    kind: "typed" as const,
    schema: z.string().trim().max(2000),
    default: (): string =>
      typeof process !== "undefined"
        ? process.env.PIPELINE_WEBHOOK_URL || process.env.WEBHOOK_URL || ""
        : "",
    parse: parseNonEmptyStringOrNull,
    serialize: (value: string | null | undefined): string | null =>
      value ?? null,
  },
  jobCompleteWebhookUrl: {
    kind: "typed" as const,
    schema: z.string().trim().max(2000),
    default: (): string =>
      typeof process !== "undefined"
        ? process.env.JOB_COMPLETE_WEBHOOK_URL || ""
        : "",
    parse: parseNonEmptyStringOrNull,
    serialize: (value: string | null | undefined): string | null =>
      value ?? null,
  },
  resumeProjects: {
    kind: "typed" as const,
    schema: resumeProjectsSchema,
    default: (): ResumeProjectsSettings => ({
      maxProjects: 20,
      lockedProjectIds: [],
      aiSelectableProjectIds: [],
    }),
    parse: (raw: string | undefined): ResumeProjectsSettings | null => {
      if (!raw) return null;
      try {
        return JSON.parse(raw);
      } catch {
        return null;
      }
    },
    serialize: (
      value: ResumeProjectsSettings | null | undefined,
    ): string | null => {
      return value ? JSON.stringify(value) : null;
    },
  },
  rxresumeMode: {
    kind: "typed" as const,
    schema: z.enum(["v4", "v5"]),
    default: (): "v4" | "v5" =>
      (typeof process !== "undefined"
        ? process.env.RXRESUME_MODE
        : undefined) === "v4"
        ? "v4"
        : "v5",
    parse: (raw: string | undefined): "v4" | "v5" | null => {
      if (!raw) return null;
      return raw === "v4" || raw === "v5" ? raw : null;
    },
    serialize: (value: "v4" | "v5" | null | undefined): string | null =>
      value ?? null,
  },
  ukvisajobsMaxJobs: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 50,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  adzunaMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number =>
      parseInt(
        typeof process !== "undefined"
          ? process.env.ADZUNA_MAX_JOBS_PER_TERM || "50"
          : "50",
        10,
      ),
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  gradcrackerMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 50,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  startupjobsMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number =>
      parseInt(
        typeof process !== "undefined"
          ? process.env.STARTUPJOBS_MAX_RESULTS || "50"
          : "50",
        10,
      ),
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  // --- New extractor caps & per-source target lists ---
  usajobsMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number =>
      parseInt(
        typeof process !== "undefined"
          ? process.env.USAJOBS_MAX_JOBS_PER_TERM || "100"
          : "100",
        10,
      ),
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  jobicyMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  themuseMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  joobleMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  careerjetMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  reedMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  remoteokMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  remotiveMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  arbeitnowMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  himalayasMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  weworkremotelyMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  workingnomadsMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  testdevjobsMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  testdevjobsMaxPages: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(20),
    default: (): number => 3,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  builtinMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  builtinMaxPagesPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(15),
    default: (): number => 3,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  wellfoundMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(200),
    default: (): number => 50,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  googleJobsMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(150),
    default: (): number => 30,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  fourdayweekMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  qajobsboardMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  /**
   * Arc.dev remote job listing paths (on https://arc.dev), e.g. /remote-jobs/playwright
   */
  arcRemoteJobsPaths: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(200)).max(20),
    default: (): string[] => {
      const fromEnv = parseCompanyList(
        typeof process !== "undefined" ? process.env.ARC_REMOTE_JOBS_PATHS : "",
      );
      return fromEnv.length > 0
        ? fromEnv
        : ["/remote-jobs/playwright", "/remote-jobs/cypress"];
    },
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  arcMaxJobsPerPath: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(300),
    default: (): number => 120,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  /**
   * Comma- or newline-separated company slugs to fetch from public ATS feeds.
   * `lever`, `ashby`, and `greenhouse` each take one entry per company.
   */
  leverCompanies: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(100)).max(200),
    default: (): string[] =>
      parseCompanyList(
        typeof process !== "undefined" ? process.env.LEVER_COMPANIES : "",
      ),
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  ashbyCompanies: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(100)).max(200),
    default: (): string[] =>
      parseCompanyList(
        typeof process !== "undefined" ? process.env.ASHBY_COMPANIES : "",
      ),
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  greenhouseCompanies: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(100)).max(200),
    default: (): string[] =>
      parseCompanyList(
        typeof process !== "undefined" ? process.env.GREENHOUSE_COMPANIES : "",
      ),
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  /**
   * Workday tenant configurations as JSON, e.g.
   * `[{"company":"Acme","tenantUrl":"https://acme.wd1.myworkdayjobs.com","sites":["External"]}]`.
   */
  workdayTenants: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(2000)).max(50),
    default: (): string[] =>
      parseCompanyList(
        typeof process !== "undefined" ? process.env.WORKDAY_TENANTS : "",
      ),
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  /**
   * SmartRecruiters company identifiers (Posting API path segment), e.g.
   * `jobs.smartrecruiters.com/smartrecruiters/...` → "smartrecruiters".
   */
  smartrecruitersCompanies: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(100)).max(200),
    default: (): string[] =>
      parseCompanyList(
        typeof process !== "undefined"
          ? process.env.SMARTRECRUITERS_COMPANIES
          : "",
      ),
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  smartrecruitersMaxJobsPerCompany: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(500),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  teamtailorCompanies: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(100)).max(200),
    default: (): string[] =>
      parseCompanyList(
        typeof process !== "undefined" ? process.env.TEAMTAILOR_COMPANIES : "",
      ),
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  huntflowTenants: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(100)).max(200),
    default: (): string[] =>
      parseCompanyList(
        typeof process !== "undefined" ? process.env.HUNTFLOW_TENANTS : "",
      ),
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  factorialTenants: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(100)).max(200),
    default: (): string[] =>
      parseCompanyList(
        typeof process !== "undefined" ? process.env.FACTORIAL_TENANTS : "",
      ),
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  careersPageUrls: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(500)).max(100),
    default: (): string[] =>
      parseCompanyList(
        typeof process !== "undefined" ? process.env.CAREERS_PAGE_URLS : "",
      ),
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  /**
   * Eluta RSS `location` query values, e.g. "Toronto, ON", "Vancouver, BC".
   * Very broad values may yield empty feeds; prefer metro/province strings.
   */
  elutaRssLocations: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(200)).max(50),
    default: (): string[] =>
      parseCompanyList(
        typeof process !== "undefined" ? process.env.ELUTA_RSS_LOCATIONS : "",
      ),
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  elutaMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number => 100,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  /**
   * Optional BC T-Net RSS URLs. When empty, the extractor uses the default public
   * aggregate feed. Env `BCTENET_RSS_URLS` seeds defaults (JSON array or newline-separated).
   */
  bctenetRssUrls: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(500)).max(20),
    default: (): string[] => {
      const raw =
        typeof process !== "undefined"
          ? process.env.BCTENET_RSS_URLS?.trim()
          : "";
      if (!raw) return [];
      const parsed = parseJsonArrayOrNull(raw);
      if (parsed && parsed.length > 0) return parsed;
      return raw
        .split(/\n+/)
        .map((piece) => piece.trim())
        .filter(Boolean);
    },
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  bctenetMaxJobsPerTerm: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(2000),
    default: (): number => 400,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  /**
   * iCIMS tenant hosts (e.g. careers-example.icims.com). Env `ICIMS_TENANTS`
   * seeds defaults (comma/newline-separated).
   */
  icimsTenants: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(200)).max(100),
    default: (): string[] =>
      parseCompanyList(
        typeof process !== "undefined" ? process.env.ICIMS_TENANTS : "",
      ),
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  icimsMaxJobsPerTenant: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(2000),
    default: (): number => 250,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  icimsMaxPagesPerSearch: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(50),
    default: (): number => 10,
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  searchTerms: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(200)).max(100),
    default: (): string[] =>
      (typeof process !== "undefined"
        ? process.env.JOBSPY_SEARCH_TERMS || "web developer"
        : "web developer"
      )
        .split("|")
        .map((v) => v.trim())
        .filter(Boolean),
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  workplaceTypes: {
    kind: "typed" as const,
    schema: z.array(z.enum(WORKPLACE_TYPE_VALUES)).min(1).max(3),
    default: (): Array<(typeof WORKPLACE_TYPE_VALUES)[number]> => [
      "remote",
      "hybrid",
      "onsite",
    ],
    parse: parseWorkplaceTypesOrNull,
    serialize: serializeNullableJsonArray,
  },
  blockedCompanyKeywords: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(200)).max(200),
    default: (): string[] => [],
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  blockedCountries: {
    kind: "typed" as const,
    schema: z.array(z.string().trim().min(1).max(100)).max(50),
    default: (): string[] => [],
    parse: parseJsonArrayOrNull,
    serialize: serializeNullableJsonArray,
  },
  scoringInstructions: {
    kind: "typed" as const,
    schema: z.string().trim().max(4000),
    default: (): string => "",
    parse: parseNonEmptyStringOrNull,
    serialize: (value: string | null | undefined): string | null =>
      value ?? null,
  },
  searchCities: {
    kind: "typed" as const,
    schema: z.string().trim().max(100),
    default: (): string =>
      typeof process !== "undefined"
        ? process.env.SEARCH_CITIES || process.env.JOBSPY_LOCATION || "UK"
        : "UK",
    parse: parseNonEmptyStringOrNull,
    serialize: (value: string | null | undefined): string | null =>
      value ?? null,
  },
  jobspyResultsWanted: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(1000),
    default: (): number =>
      parseInt(
        typeof process !== "undefined"
          ? process.env.JOBSPY_RESULTS_WANTED || "200"
          : "200",
        10,
      ),
    parse: parseIntOrNull,
    serialize: serializeNullableNumber,
  },
  jobspyCountryIndeed: {
    kind: "typed" as const,
    schema: z.string().trim().max(100),
    default: (): string =>
      typeof process !== "undefined"
        ? process.env.JOBSPY_COUNTRY_INDEED || "UK"
        : "UK",
    parse: parseNonEmptyStringOrNull,
    serialize: (value: string | null | undefined): string | null =>
      value ?? null,
  },
  showSponsorInfo: {
    kind: "typed" as const,
    schema: z.boolean(),
    default: (): boolean => true,
    parse: parseBitBoolOrNull,
    serialize: serializeBitBool,
  },
  renderMarkdownInJobDescriptions: {
    kind: "typed" as const,
    schema: z.boolean(),
    default: (): boolean => true,
    parse: parseBitBoolOrNull,
    serialize: serializeBitBool,
  },
  chatStyleTone: {
    kind: "typed" as const,
    schema: z.string().trim().max(100),
    default: (): string =>
      typeof process !== "undefined"
        ? process.env.CHAT_STYLE_TONE || "professional"
        : "professional",
    parse: parseNonEmptyStringOrNull,
    serialize: (value: string | null | undefined): string | null =>
      value ?? null,
  },
  chatStyleFormality: {
    kind: "typed" as const,
    schema: z.string().trim().max(100),
    default: (): string =>
      typeof process !== "undefined"
        ? process.env.CHAT_STYLE_FORMALITY || "medium"
        : "medium",
    parse: parseNonEmptyStringOrNull,
    serialize: (value: string | null | undefined): string | null =>
      value ?? null,
  },
  chatStyleConstraints: {
    kind: "typed" as const,
    schema: z.string().trim().max(4000),
    default: (): string =>
      typeof process !== "undefined"
        ? process.env.CHAT_STYLE_CONSTRAINTS || ""
        : "",
    parse: parseNonEmptyStringOrNull,
    serialize: (value: string | null | undefined): string | null =>
      value ?? null,
  },
  chatStyleDoNotUse: {
    kind: "typed" as const,
    schema: z.string().trim().max(1000),
    default: (): string =>
      typeof process !== "undefined"
        ? process.env.CHAT_STYLE_DO_NOT_USE || ""
        : "",
    parse: parseNonEmptyStringOrNull,
    serialize: (value: string | null | undefined): string | null =>
      value ?? null,
  },
  chatStyleLanguageMode: {
    kind: "typed" as const,
    schema: z.enum(CHAT_STYLE_LANGUAGE_MODE_VALUES),
    default: (): ChatStyleLanguageMode =>
      parseChatStyleLanguageModeOrNull(
        typeof process !== "undefined"
          ? process.env.CHAT_STYLE_LANGUAGE_MODE
          : undefined,
      ) ?? "manual",
    parse: parseChatStyleLanguageModeOrNull,
    serialize: (
      value: ChatStyleLanguageMode | null | undefined,
    ): string | null => value ?? null,
  },
  chatStyleManualLanguage: {
    kind: "typed" as const,
    schema: z.enum(CHAT_STYLE_MANUAL_LANGUAGE_VALUES),
    default: (): ChatStyleManualLanguage =>
      parseChatStyleManualLanguageOrNull(
        typeof process !== "undefined"
          ? process.env.CHAT_STYLE_MANUAL_LANGUAGE
          : undefined,
      ) ?? "english",
    parse: parseChatStyleManualLanguageOrNull,
    serialize: (
      value: ChatStyleManualLanguage | null | undefined,
    ): string | null => value ?? null,
  },
  backupEnabled: {
    kind: "typed" as const,
    schema: z.boolean(),
    default: (): boolean => false,
    parse: parseBitBoolOrNull,
    serialize: serializeBitBool,
  },
  backupHour: {
    kind: "typed" as const,
    schema: z.number().int().min(0).max(23),
    default: (): number => 2,
    parse: (raw: string | undefined): number | null => {
      const parsed = raw ? parseInt(raw, 10) : NaN;
      if (Number.isNaN(parsed)) return null;
      return Math.min(23, Math.max(0, parsed));
    },
    serialize: serializeNullableNumber,
  },
  backupMaxCount: {
    kind: "typed" as const,
    schema: z.number().int().min(1).max(5),
    default: (): number => 5,
    parse: (raw: string | undefined): number | null => {
      const parsed = raw ? parseInt(raw, 10) : NaN;
      if (Number.isNaN(parsed)) return null;
      return Math.min(5, Math.max(1, parsed));
    },
    serialize: serializeNullableNumber,
  },
  penalizeMissingSalary: {
    kind: "typed" as const,
    schema: z.boolean(),
    default: (): boolean => {
      if (typeof process === "undefined") return false;
      const v = process.env.PENALIZE_MISSING_SALARY || "0";
      return v === "1" || v.toLowerCase() === "true";
    },
    parse: parseBitBoolOrNull,
    serialize: serializeBitBool,
  },
  missingSalaryPenalty: {
    kind: "typed" as const,
    schema: z.number().int().min(0).max(100),
    default: (): number => {
      if (typeof process === "undefined") return 10;
      const raw = process.env.MISSING_SALARY_PENALTY;
      if (!raw) return 10;
      const parsed = parseInt(raw, 10);
      return Number.isNaN(parsed) ? 10 : Math.min(100, Math.max(0, parsed));
    },
    parse: (raw: string | undefined): number | null => {
      const parsed = raw ? parseInt(raw, 10) : NaN;
      return Number.isNaN(parsed) ? null : Math.min(100, Math.max(0, parsed));
    },
    serialize: serializeNullableNumber,
  },
  autoSkipScoreThreshold: {
    kind: "typed" as const,
    schema: z.number().int().min(0).max(100),
    default: (): number | null => null,
    parse: (raw: string | undefined): number | null => {
      if (!raw || raw === "null" || raw === "") return null;
      const parsed = parseInt(raw, 10);
      return Number.isNaN(parsed) ? null : Math.min(100, Math.max(0, parsed));
    },
    serialize: (value: number | null | undefined): string | null => {
      return value === null || value === undefined ? null : String(value);
    },
  },

  // --- Model Variants ---
  modelScorer: {
    kind: "model" as const,
    schema: z.string().trim().max(200),
  },
  modelTailoring: {
    kind: "model" as const,
    schema: z.string().trim().max(200),
  },
  modelProjectSelection: {
    kind: "model" as const,
    schema: z.string().trim().max(200),
  },

  // --- Simple Strings ---
  activeProfileId: {
    kind: "string" as const,
    schema: z.string().trim().max(200),
  },
  basicAuthActiveProfileByUser: {
    kind: "string" as const,
    schema: z.string().trim().max(10000),
  },
  rxresumeBaseResumeId: {
    kind: "string" as const,
    schema: z.string().trim().max(200),
  },
  rxresumeBaseResumeIdV4: {
    kind: "string" as const,
    schema: z.string().trim().max(200),
  },
  rxresumeBaseResumeIdV5: {
    kind: "string" as const,
    schema: z.string().trim().max(200),
  },
  rxresumeEmail: {
    kind: "string" as const,
    envKey: "RXRESUME_EMAIL",
    schema: z.string().trim().max(200),
  },
  rxresumeUrl: {
    kind: "string" as const,
    envKey: "RXRESUME_URL",
    schema: z.preprocess(
      (value) => (value === "" ? null : value),
      z.string().trim().url().max(2000).nullable(),
    ),
  },
  /** Server path to Reactive Resume JSON export; used when RxResume API is not available. */
  localResumeProfilePath: {
    kind: "string" as const,
    schema: z.string().trim().max(4000),
  },
  ukvisajobsEmail: {
    kind: "string" as const,
    envKey: "UKVISAJOBS_EMAIL",
    schema: z.string().trim().max(200),
  },
  adzunaAppId: {
    kind: "string" as const,
    envKey: "ADZUNA_APP_ID",
    schema: z.string().trim().max(200),
  },
  // --- New extractor keys / identifiers (non-secret) ---
  usajobsUserAgent: {
    kind: "string" as const,
    envKey: "USAJOBS_USER_AGENT",
    schema: z.string().trim().max(200),
  },
  themuseApiKey: {
    kind: "string" as const,
    envKey: "THEMUSE_API_KEY",
    schema: z.string().trim().max(200),
  },
  /** Publisher API key (Basic auth user); Careerjet labels this “API key” in the dashboard. */
  careerjetAffid: {
    kind: "string" as const,
    envKey: "CAREERJET_AFFID",
    schema: z.string().trim().max(200),
  },
  /** Required Referer URL for v4 (your job-search page that triggers API use). */
  careerjetReferer: {
    kind: "string" as const,
    envKey: "CAREERJET_REFERER",
    schema: z.string().trim().max(500),
  },
  /** Must match an IP allowlisted in Careerjet (usually your server egress IP). */
  careerjetUserIp: {
    kind: "string" as const,
    envKey: "CAREERJET_USER_IP",
    schema: z.string().trim().max(80),
  },
  careerjetUserAgent: {
    kind: "string" as const,
    envKey: "CAREERJET_USER_AGENT",
    schema: z.string().trim().max(500),
  },
  basicAuthUser: {
    kind: "string" as const,
    envKey: "BASIC_AUTH_USER",
    schema: z.string().trim().max(200),
  },

  // --- Secrets ---
  llmApiKey: {
    kind: "secret" as const,
    envKey: "LLM_API_KEY",
    schema: z.string().trim().max(2000),
  },
  rxresumePassword: {
    kind: "secret" as const,
    envKey: "RXRESUME_PASSWORD",
    schema: z.string().trim().max(2000),
  },
  rxresumeApiKey: {
    kind: "secret" as const,
    envKey: "RXRESUME_API_KEY",
    schema: z.string().trim().max(2000),
  },
  ukvisajobsPassword: {
    kind: "secret" as const,
    envKey: "UKVISAJOBS_PASSWORD",
    schema: z.string().trim().max(2000),
  },
  adzunaAppKey: {
    kind: "secret" as const,
    envKey: "ADZUNA_APP_KEY",
    schema: z.string().trim().max(2000),
  },
  // --- Secrets for new extractors ---
  usajobsApiKey: {
    kind: "secret" as const,
    envKey: "USAJOBS_API_KEY",
    schema: z.string().trim().max(2000),
  },
  joobleApiKey: {
    kind: "secret" as const,
    envKey: "JOOBLE_API_KEY",
    schema: z.string().trim().max(2000),
  },
  reedApiKey: {
    kind: "secret" as const,
    envKey: "REED_API_KEY",
    schema: z.string().trim().max(2000),
  },
  basicAuthPassword: {
    kind: "secret" as const,
    envKey: "BASIC_AUTH_PASSWORD",
    schema: z.string().trim().max(2000),
  },
  webhookSecret: {
    kind: "secret" as const,
    envKey: "WEBHOOK_SECRET",
    schema: z.string().trim().max(2000),
  },

  // --- Aliases ---
  jobspyLocation: {
    kind: "alias" as const,
    schema: z.string().trim().max(100),
    target: "searchCities" as const,
  },

  // --- Virtual ---
  enableBasicAuth: {
    kind: "virtual" as const,
    schema: z.boolean(),
  },
} as const;

export type SettingsRegistry = typeof settingsRegistry;
export type SettingsRegistryKey = keyof SettingsRegistry;
