import { createProviderError, isProviderReasonCode, providerErrorCodeFromReasonCode } from "./errors";
import type { JsonValue, NormalizedRecord, ProviderFailureEntry, ProviderReasonCode, ProviderSource } from "./types";
import type { SiteRecipe, SiteRecipeBadState } from "../guidance/types";

export type BrowserNativeDiscoveryResult = {
  records: NormalizedRecord[];
  failures: ProviderFailureEntry[];
  diagnostics: Record<string, JsonValue>;
};

export type BrowserNativeDiscoveryInput = {
  recipe: SiteRecipe;
  query: string;
  maxReferences: number;
  browserMode?: string;
  useCookies?: boolean;
  cookiePolicy?: string;
  fetchSearchPage?: (url: string) => Promise<BrowserNativeDiscoveryFetchResult>;
};

export type BrowserNativeDiscoveryFetchResult = {
  records: NormalizedRecord[];
  failures: ProviderFailureEntry[];
  errorMessage?: string;
};

type MatchedBadState = {
  state: SiteRecipeBadState;
  reasonCode: ProviderReasonCode;
};

type BadStateMatchMode = "hard_blocker" | "pre_extraction" | "all";

const HARD_FAILURE_REASON_CODES = new Set<ProviderReasonCode>([
  "auth_required",
  "challenge_detected",
  "policy_blocked",
  "rate_limited",
  "token_required"
]);

const sourceForRecipe = (recipe: SiteRecipe): ProviderSource => {
  return recipe.id.startsWith("social/") ? "social" : "web";
};

const needsAuthenticatedBrowser = (input: BrowserNativeDiscoveryInput): boolean => {
  if (input.recipe.authMode === "public") return false;
  if (input.cookiePolicy === "required") return true;
  return input.recipe.authMode === "authenticated";
};

const hasAuthenticatedBrowser = (input: BrowserNativeDiscoveryInput): boolean => {
  return input.browserMode === "extension" && input.useCookies === true;
};

const htmlFromRecord = (record: NormalizedRecord): string => {
  const html = record.attributes.html;
  return typeof html === "string" ? html : "";
};

const linksFromRecord = (record: NormalizedRecord): string[] => {
  const links = record.attributes.links;
  return Array.isArray(links) ? links.filter((link): link is string => typeof link === "string") : [];
};

const badStateTextForRecord = (record: NormalizedRecord): string => {
  return [
    record.url ?? "",
    record.title ?? "",
    record.content ?? "",
    htmlFromRecord(record)
  ].join(" ").toLowerCase();
};

const normalizeBadStateReasonCode = (state: SiteRecipeBadState): ProviderReasonCode => {
  return isProviderReasonCode(state.reasonCode) ? state.reasonCode : "env_limited";
};

const shouldMatchBadState = (state: SiteRecipeBadState, mode: BadStateMatchMode): boolean => {
  if (mode === "hard_blocker") return state.id === "challenge";
  return mode === "all" || state.id !== "search-shell";
};

const findBadState = (
  recipe: SiteRecipe,
  records: NormalizedRecord[],
  mode: BadStateMatchMode
): MatchedBadState | undefined => {
  for (const record of records) {
    const text = badStateTextForRecord(record);
    const state = recipe.badStates.find((candidate) => (
      shouldMatchBadState(candidate, mode)
      && candidate.markers.some((marker) => text.includes(marker.toLowerCase()))
    ));
    if (state) {
      return {
        state,
        reasonCode: normalizeBadStateReasonCode(state)
      };
    }
  }
  return undefined;
};

const findHardFailure = (failures: ProviderFailureEntry[]): ProviderFailureEntry | undefined => (
  failures.find((failure) => {
    const reasonCode = failure.error.reasonCode ?? failure.error.details?.reasonCode;
    return typeof reasonCode === "string"
      && isProviderReasonCode(reasonCode)
      && HARD_FAILURE_REASON_CODES.has(reasonCode);
  })
);

const reasonCodeForFailure = (failure: ProviderFailureEntry): ProviderReasonCode | undefined => {
  const reasonCode = failure.error.reasonCode ?? failure.error.details?.reasonCode;
  return typeof reasonCode === "string" && isProviderReasonCode(reasonCode) ? reasonCode : undefined;
};

const buildBadStateResult = (
  input: BrowserNativeDiscoveryInput,
  source: ProviderSource,
  searchUrl: string,
  fetchedRecordCount: number,
  badState: MatchedBadState
): BrowserNativeDiscoveryResult => ({
  records: [],
  failures: [{
    provider: input.recipe.id,
    source,
    error: createProviderError(
      providerErrorCodeFromReasonCode(badState.reasonCode),
      badState.state.recoveryAction,
      {
        retryable: true,
        reasonCode: badState.reasonCode,
        provider: input.recipe.id,
        source,
        details: {
          siteRecipeId: input.recipe.id,
          query: input.query,
          searchUrl,
          badStateId: badState.state.id
        }
      }
    )
  }],
  diagnostics: {
    siteRecipeId: input.recipe.id,
    attempted: true,
    reason: badState.reasonCode,
    searchUrl,
    fetchedRecordCount,
    badStateId: badState.state.id,
    recoveryAction: badState.state.recoveryAction
  }
});

const buildHardFailureResult = (
  input: BrowserNativeDiscoveryInput,
  source: ProviderSource,
  searchUrl: string,
  fetchedRecordCount: number,
  failure: ProviderFailureEntry
): BrowserNativeDiscoveryResult => ({
  records: [],
  failures: [failure],
  diagnostics: {
    siteRecipeId: input.recipe.id,
    attempted: true,
    reason: reasonCodeForFailure(failure) ?? "challenge_detected",
    searchUrl,
    fetchedRecordCount,
    recoveryAction: failure.error.message
  }
});

const extractRecipeReferenceUrls = (
  input: BrowserNativeDiscoveryInput,
  records: NormalizedRecord[],
  maxReferences: number
): string[] => {
  const extractor = input.recipe.browserNativeDiscovery?.extractReferenceUrls;
  if (!extractor) return [];
  const urls: string[] = [];
  const seen = new Set<string>();
  const pushUrl = (url: string): void => {
    if (!url || seen.has(url)) return;
    seen.add(url);
    urls.push(url);
  };
  for (const record of records) {
    extractor({
      url: record.url ?? undefined,
      content: record.content ?? undefined,
      html: typeof record.attributes.html === "string" ? record.attributes.html : undefined,
      links: linksFromRecord(record)
    }).forEach(pushUrl);
    if (urls.length >= maxReferences) break;
  }
  return urls.slice(0, maxReferences);
};

const buildRecipeReferenceRecord = (
  input: BrowserNativeDiscoveryInput,
  url: string,
  index: number
): NormalizedRecord => {
  const timestamp = new Date().toISOString();
  return {
    id: `${input.recipe.id}:reference:${index + 1}:${timestamp}`,
    source: sourceForRecipe(input.recipe),
    provider: input.recipe.id,
    url,
    title: `Site visual reference ${index + 1} for ${input.query}`,
    content: input.recipe.navigationSteps.map((step) => step.instruction).join(" "),
    timestamp,
    confidence: 0.72,
    attributes: {
      siteRecipeId: input.recipe.id,
      discoveryMode: "browser_native_extracted_reference",
      authMode: input.recipe.authMode,
      maxReferences: input.maxReferences,
      validationChecks: input.recipe.evidenceRequirements.map((entry) => entry.validation)
    }
  };
};

export const runBrowserNativeDiscovery = async (
  input: BrowserNativeDiscoveryInput
): Promise<BrowserNativeDiscoveryResult> => {
  const source = sourceForRecipe(input.recipe);
  const searchUrl = input.recipe.browserNativeDiscovery?.buildSearchUrl(input.query);
  if (!searchUrl) {
    return {
      records: [],
      failures: [{
        provider: input.recipe.id,
        source,
        error: createProviderError(
          "unavailable",
          `Site recipe ${input.recipe.id} does not define browser-native discovery.`,
          {
            retryable: false,
            reasonCode: "env_limited",
            provider: input.recipe.id,
            source,
            details: {
              siteRecipeId: input.recipe.id,
              query: input.query
            }
          }
        )
      }],
      diagnostics: {
        siteRecipeId: input.recipe.id,
        attempted: false,
        reason: "unsupported_site_recipe"
      }
    };
  }
  if (needsAuthenticatedBrowser(input) && !hasAuthenticatedBrowser(input)) {
    return {
      records: [],
      failures: [{
        provider: input.recipe.id,
        source,
        error: createProviderError(
          "auth",
          `${input.recipe.id} requires an authenticated browser session before search results are visible.`,
          {
            retryable: true,
            reasonCode: "auth_required",
            provider: input.recipe.id,
            source,
            details: {
              siteRecipeId: input.recipe.id,
              query: input.query,
              requiredBrowserMode: "extension",
              requiredCookies: true
            }
          }
        )
      }],
      diagnostics: {
        siteRecipeId: input.recipe.id,
        attempted: false,
        reason: "auth_required",
        recoverySteps: input.recipe.recoverySteps.map((step) => step.instruction)
      }
    };
  }

  if (!input.fetchSearchPage) {
    return {
      records: [],
      failures: [{
        provider: input.recipe.id,
        source,
        error: createProviderError(
          "unavailable",
          `${input.recipe.id} browser-native discovery requires a browser-backed fetch executor for the search page.`,
          {
            retryable: true,
            reasonCode: "env_limited",
            provider: input.recipe.id,
            source,
            details: {
              siteRecipeId: input.recipe.id,
              query: input.query,
              searchUrl
            }
          }
        )
      }],
      diagnostics: {
        siteRecipeId: input.recipe.id,
        attempted: false,
        reason: "fetch_executor_missing",
        searchUrl
      }
    };
  }

  const fetched = await input.fetchSearchPage(searchUrl);
  const hardFailure = findHardFailure(fetched.failures);
  if (hardFailure) {
    return buildHardFailureResult(input, source, searchUrl, fetched.records.length, hardFailure);
  }
  const preExtractionMode = needsAuthenticatedBrowser(input) ? "pre_extraction" : "hard_blocker";
  const hardBlocker = findBadState(input.recipe, fetched.records, preExtractionMode);
  if (hardBlocker) {
    return buildBadStateResult(input, source, searchUrl, fetched.records.length, hardBlocker);
  }
  const extractedUrls = extractRecipeReferenceUrls(input, fetched.records, input.maxReferences);
  if (extractedUrls.length === 0) {
    const shellState = fetched.failures.length === 0 ? findBadState(input.recipe, fetched.records, "all") : undefined;
    if (shellState) {
      return buildBadStateResult(input, source, searchUrl, fetched.records.length, shellState);
    }
    return {
      records: [],
      failures: fetched.failures.length > 0
        ? fetched.failures
        : [{
          provider: input.recipe.id,
          source,
          error: createProviderError(
            "unavailable",
            fetched.errorMessage ?? `${input.recipe.id} search did not expose recipe-approved URLs that can be used as references.`,
            {
              retryable: true,
              reasonCode: "env_limited",
              provider: input.recipe.id,
              source,
              details: {
                siteRecipeId: input.recipe.id,
                query: input.query,
                searchUrl
              }
            }
          )
        }],
      diagnostics: {
        siteRecipeId: input.recipe.id,
        attempted: true,
        reason: "no_reference_urls_extracted",
        searchUrl,
        fetchedRecordCount: fetched.records.length
      }
    };
  }

  return {
    records: extractedUrls.map((url, index) => buildRecipeReferenceRecord(input, url, index)),
    failures: [],
    diagnostics: {
      siteRecipeId: input.recipe.id,
      attempted: true,
      reason: "reference_urls_extracted",
      searchUrl,
      navigationSteps: input.recipe.navigationSteps.map((step) => step.instruction),
      badStates: input.recipe.badStates.map((state) => state.id),
      extractedUrlCount: extractedUrls.length
    }
  };
};
