"use client";

import { useEffect, useMemo, useRef, useState } from "react";
import { apiClient } from "@/lib/api/client";
import type { FieldOption, FieldRef } from "@/types/invyone-component";

/**
 * 로더가 반환하는 옵션 형식 — select-pickers 의 SelectOption 과 호환.
 *
 * FieldOption 은 `string | {value,label}` union 이라 picker 들이 그대로 받기
 * 어려움. 로더 내부에서 객체 형태로 정규화한 결과를 반환한다.
 */
export interface LoadedOption {
  value: string;
  label: string;
}

/**
 * use-option-loader
 *
 * InputComponent 의 select 계열 (single / multi / radio / check / tag) 의 옵션을
 * 5 가지 source 에서 비동기로 로드한다.
 *
 *   static    — config.options 그대로 사용
 *   code      — 공통코드 (/common-codes/detail?code_info={group}) — code_group / codeInfo
 *   category  — 사용자 정의 카테고리 (/table-categories/{table}/{column}/values)
 *   distinct  — 현재 테이블 컬럼의 distinct (/entity/{table}/distinct/{column})
 *   api       — 임의 endpoint
 *
 * 옛 선택 컴포넌트의 옵션 로딩 로직을 input canonical 용으로 정리. apiClient 만 의존.
 *
 * 디자인 모드 가드 — isDesignMode === true 면 static 만 처리하고 API 호출은 모두
 * skip. 캔버스 위에서 옵션 컴포넌트를 끌어 놓는 동안 API 폭주를 방지.
 *
 * 캐시 — 같은 url 결과를 module-scoped Map 에 저장 (브라우저 세션 동안). race 조건
 * 방지로 effect 내 cancelled flag 사용.
 *
 * 계층 / cascade — code source 의 단일 hierarchical (parentField 의 value 로 자식
 * 코드 조회) 까지만 지원. swap / 다단 cascade 는 TODO.
 *
 * 사용 예:
 *   const { options, loading } = useOptionLoader({
 *     config: componentConfig,
 *     tableName,
 *     columnName,
 *     formData,
 *     isDesignMode,
 *   });
 */

/**
 * 옵션 검색 필터 (canonical) — InvFieldConfigPanel 의 옵션 필터 UI 가 이 형태로
 * 저장한다. 옛 V2 입력/선택 본체가 삭제되며 canonical 위치로 이전됨.
 *
 * value_type:
 *   static  — value 가 고정값
 *   field   — field_ref 가 다른 폼 필드명, runtime 에 그 필드 값 치환
 *   user    — user_field 가 로그인 사용자 메타 (companyCode 등), runtime 에 치환
 */
export interface OptionFilter {
  column: string;
  operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "in" | "notIn" | "like" | "isNull" | "isNotNull";
  value_type?: "static" | "field" | "user";
  value?: unknown;
  field_ref?: string;
  user_field?: "companyCode" | "userId" | "deptCode" | "userName";
}

export type OptionSource =
  | "static"
  | "code"
  | "category"
  | "entity"
  | "distinct"
  | "select"
  | "api"
  | "db";

interface RawOptionItem {
  value?: unknown;
  label?: unknown;
  // camelCase (legacy / 일부 endpoint)
  valueCode?: string;
  valueLabel?: string;
  // snake_case (백엔드 표준 응답 — table-categories / common-codes hierarchy)
  value_code?: string;
  value_label?: string;
  children?: RawOptionItem[];
}

export interface OptionLoaderConfig {
  source?: string;
  options?: Array<FieldOption | { value: string; label?: string }> | undefined;

  codeGroup?: string;
  codeInfo?: string;

  categoryTable?: string;
  categoryColumn?: string;

  entityTable?: string;
  entityValueColumn?: string;
  entityLabelColumn?: string;
  ref?: FieldRef;

  table?: string;
  valueColumn?: string;
  labelColumn?: string;

  apiEndpoint?: string;

  hierarchical?: boolean;
  parentField?: string;

  /**
   * 옵션 검색 필터 (canonical) — runtime 에 formData / user context 로 치환된 후
   * 백엔드의 `filters` query 파라미터에 JSON 문자열로 전달됨.
   */
  filters?: OptionFilter[];
}

/**
 * 로그인 사용자 메타 — `value_type === "user"` 필터 치환에 사용.
 */
export interface OptionUserContext {
  companyCode?: string;
  userId?: string;
  deptCode?: string;
  userName?: string;
}

export interface UseOptionLoaderArgs {
  config: OptionLoaderConfig & Record<string, any>;
  tableName?: string;
  columnName?: string;
  formData?: Record<string, any>;
  /** `value_type === "user"` 필터 치환용 사용자 메타. 미지정 시 user 필터는 skip. */
  userContext?: OptionUserContext;
  isDesignMode?: boolean;
}

export interface UseOptionLoaderResult {
  options: LoadedOption[];
  loading: boolean;
}

// FieldOption (string | object) 도 받을 수 있게 normalizeOptions 가 처리.
// FieldOption 은 import 만 하고 직접 시그니처에는 노출하지 않는다.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type _FieldOptionSentinel = FieldOption;

// 모듈 스코프 캐시 — url 단위. apiClient 의 baseURL 이 같다는 전제.
// 캐시 무효화 트리거가 필요한 경우 입력 컴포넌트가 unmount/remount 되어야 함.
const responseCache = new Map<string, LoadedOption[]>();

function normalizeOptions(raw: any[]): LoadedOption[] {
  if (!Array.isArray(raw)) return [];
  return raw
    .map((item): LoadedOption | null => {
      if (item == null) return null;
      if (typeof item === "string") return { value: item, label: item };
      if (typeof item === "object") {
        // 값 키 우선순위: value / code / id (보편)
        //   + valueCode / value_code (table-categories tree leaf)
        //   + codeValue / code_value (common-codes hierarchy)
        const v =
          item.value ??
          item.code ??
          item.id ??
          item.valueCode ??
          item.value_code ??
          item.codeValue ??
          item.code_value;
        // 라벨 키 우선순위: label / name (보편)
        //   + valueLabel / value_label (table-categories)
        //   + codeName / code_name (common-codes hierarchy)
        const l =
          item.label ??
          item.name ??
          item.valueLabel ??
          item.value_label ??
          item.codeName ??
          item.code_name ??
          v;
        if (v == null || String(v) === "") return null;
        return { value: String(v), label: l != null ? String(l) : String(v) };
      }
      return null;
    })
    .filter((x): x is LoadedOption => x !== null);
}

function flattenCategoryTree(items: RawOptionItem[], depth = 0): LoadedOption[] {
  const out: LoadedOption[] = [];
  for (const item of items) {
    // 백엔드 응답은 snake_case (value_code/value_label) — camelCase 호환 유지.
    const code =
      item.valueCode ??
      item.value_code ??
      (item.value != null ? String(item.value) : "");
    if (!code) continue;
    const labelRaw =
      item.valueLabel ??
      item.value_label ??
      (item.label != null ? String(item.label) : code);
    const prefix = depth > 0 ? "   ".repeat(depth) + "└ " : "";
    out.push({ value: code, label: prefix + labelRaw });
    if (Array.isArray(item.children) && item.children.length > 0) {
      out.push(...flattenCategoryTree(item.children, depth + 1));
    }
  }
  return out;
}

function isCanonicalEmpty(v: any): boolean {
  return v === undefined || v === null || v === "";
}

/** ResolvedFilter — runtime 에 value 가 실제 값으로 치환된 후의 형태. */
interface ResolvedFilter {
  column: string;
  operator: OptionFilter["operator"];
  /** isNull/isNotNull 은 value 없이 전달. in/notIn 은 배열. */
  value?: unknown;
}

/**
 * OptionFilter 배열을 runtime 값으로 치환.
 * - column 빈 값 → skip
 * - value_type=field: formData[field_ref] 값으로 치환. 값 없으면 skip
 *     (단 isNull/isNotNull 은 value 없이 통과)
 * - value_type=user: userContext[user_field] 로 치환. 값 없으면 skip
 * - in/notIn: 배열 또는 "a,b,c" 문자열 모두 trim 된 배열로 정규화
 * - isNull/isNotNull: value 없이 항상 통과
 */
function resolveFilters(
  filters: OptionFilter[] | undefined,
  formData: Record<string, any> | undefined,
  userContext: OptionUserContext | undefined,
): ResolvedFilter[] {
  if (!Array.isArray(filters) || filters.length === 0) return [];
  const out: ResolvedFilter[] = [];
  for (const f of filters) {
    const column = typeof f?.column === "string" ? f.column.trim() : "";
    if (!column) continue;
    const op = f.operator;

    // isNull / isNotNull 은 value 없이 통과
    if (op === "isNull" || op === "isNotNull") {
      out.push({ column, operator: op });
      continue;
    }

    let raw: unknown;
    const vt = f.value_type || "static";
    if (vt === "static") {
      raw = f.value;
    } else if (vt === "field") {
      if (!f.field_ref) continue;
      raw = formData?.[f.field_ref];
      if (isCanonicalEmpty(raw)) continue;
    } else if (vt === "user") {
      if (!f.user_field) continue;
      raw = userContext?.[f.user_field];
      if (isCanonicalEmpty(raw)) continue;
    } else {
      raw = f.value;
    }

    // in / notIn 정규화: 배열 또는 콤마 구분 문자열
    if (op === "in" || op === "notIn") {
      let arr: unknown[];
      if (Array.isArray(raw)) {
        arr = raw;
      } else if (typeof raw === "string") {
        arr = raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
      } else if (isCanonicalEmpty(raw)) {
        continue;
      } else {
        arr = [raw];
      }
      if (arr.length === 0) continue;
      out.push({ column, operator: op, value: arr });
      continue;
    }

    // 그 외 operator 는 빈 값 skip
    if (isCanonicalEmpty(raw)) continue;
    out.push({ column, operator: op, value: raw });
  }
  return out;
}

function resolveSource(config: OptionLoaderConfig): OptionSource {
  const raw = (config.source || "").toString().toLowerCase();
  if (raw === "static" || raw === "code" || raw === "category" || raw === "entity" ||
      raw === "distinct" || raw === "select" || raw === "api" || raw === "db") {
    return raw as OptionSource;
  }
  // source 미지정 — 기존 데이터를 보고 추정 (옛 키 호환 최소)
  if (Array.isArray(config.options) && config.options.length > 0) return "static";
  if (config.codeGroup || config.codeInfo) return "code";
  if (config.categoryTable && config.categoryColumn) return "category";
  if (config.entityTable || config.ref?.table) return "entity";
  if (config.apiEndpoint) return "api";
  return "static";
}

export function useOptionLoader({
  config,
  tableName,
  columnName,
  formData,
  userContext,
  isDesignMode,
}: UseOptionLoaderArgs): UseOptionLoaderResult {
  const source = resolveSource(config);

  // static options 는 외부 의존성 없이 즉시 결정.
  const staticOptions = useMemo<LoadedOption[]>(() => {
    return normalizeOptions(config.options as any[] | undefined ?? []);
  }, [config.options]);

  // runtime 치환된 filter JSON — fetchUrl 의 dep 으로 stable string 사용해서
  // formData / filters 객체 reference 변경에 의한 effect 폭주 방지.
  // 결과가 빈 배열이면 undefined 로 두어 URL 에 filters query 자체를 안 붙임.
  const resolvedFiltersJson = useMemo<string | undefined>(() => {
    const resolved = resolveFilters(config.filters, formData, userContext);
    if (resolved.length === 0) return undefined;
    return JSON.stringify(resolved);
  }, [
    config.filters,
    formData,
    userContext?.companyCode,
    userContext?.userId,
    userContext?.deptCode,
    userContext?.userName,
  ]);

  // 계층 코드의 parent 값 (다른 폼 필드 참조)
  const parentValue = useMemo<string | undefined>(() => {
    if (!config.hierarchical || !config.parentField) return undefined;
    const v = formData?.[config.parentField];
    if (v == null || v === "") return undefined;
    return String(v);
  }, [config.hierarchical, config.parentField, formData]);

  // API 호출이 필요한 source 인지
  const needsFetch =
    source !== "static" &&
    // 디자인 모드에서는 캔버스 드래그/리사이즈 등으로 effect 가 자주 트리거되므로
    // API 호출을 통째로 skip 한다. 운영 모드에서만 fetch.
    !isDesignMode;

  // fetch url 결정 (memo 로 effect dep 안정화)
  const fetchUrl = useMemo<string | null>(() => {
    if (!needsFetch) return null;
    // filters query 헬퍼 — 이미 resolvedFiltersJson 으로 stable string 화 되어 있음.
    // 백엔드 표준: `filters=` URL-encoded JSON. 빈 결과면 query 자체를 안 붙임.
    const filtersQuery = resolvedFiltersJson
      ? `filters=${encodeURIComponent(resolvedFiltersJson)}`
      : "";
    const appendFilters = (url: string): string => {
      if (!filtersQuery) return url;
      return url + (url.includes("?") ? "&" : "?") + filtersQuery;
    };

    if (source === "code") {
      const group = config.codeGroup || config.codeInfo;
      if (!group) return null;
      // 새 구조 (code_info 마스터 + code_detail 트리). BE 가 트리 전체를 평탄화 반환.
      // hierarchical/parentValue 단일 레벨 필터는 추후 명세 받으면 추가.
      return `/common-codes/detail?code_info=${encodeURIComponent(group)}`;
    }
    if (source === "category") {
      const t = config.categoryTable || tableName;
      const c = config.categoryColumn || columnName;
      if (!t || !c) return null;
      // filters 는 table-categories endpoint 가 처리 안 함 → query 미추가
      return `/table-categories/${encodeURIComponent(t)}/${encodeURIComponent(c)}/values`;
    }
    if (source === "distinct" || source === "select") {
      // tableName 필수, columnName 은 가상컬럼 (comp_*) 제외
      if (!tableName || !columnName) return null;
      if (columnName.startsWith("comp_")) return null;
      return appendFilters(
        `/entity/${encodeURIComponent(tableName)}/distinct/${encodeURIComponent(columnName)}`,
      );
    }
    if (source === "api") {
      const ep = config.apiEndpoint;
      if (!ep) return null;
      // 외부 endpoint 는 filter 스펙이 정의되어 있지 않으므로 통과 (사용자 정의 URL 그대로)
      return ep;
    }
    if (source === "entity") {
      // entity 는 DB FK 를 직접 걸 수 없는 경우 화면 설정에서 참조 테이블과
      // value/label 컬럼을 지정해 code-name 옵션으로 읽는다.
      const ref = config.ref;
      const t = config.entityTable || ref?.table;
      if (!t) return null;
      const v = config.entityValueColumn || ref?.valueColumn || "id";
      const l = config.entityLabelColumn || ref?.displayColumn || "name";
      return appendFilters(
        `/entity/${encodeURIComponent(t)}/options?value=${encodeURIComponent(v)}&label=${encodeURIComponent(l)}`,
      );
    }
    if (source === "db") {
      // legacy V2 source. table/value/label 컬럼으로 옵션 펼치기.
      const t = config.table;
      if (!t) return null;
      const v = config.valueColumn || "id";
      const l = config.labelColumn || "name";
      return appendFilters(
        `/entity/${encodeURIComponent(t)}/options?value=${encodeURIComponent(v)}&label=${encodeURIComponent(l)}`,
      );
    }
    return null;
  }, [
    needsFetch,
    source,
    config.codeGroup,
    config.codeInfo,
    config.hierarchical,
    parentValue,
    config.categoryTable,
    config.categoryColumn,
    config.apiEndpoint,
    config.table,
    config.valueColumn,
    config.labelColumn,
    // entity 분기 의존성 — entityTable / entityValueColumn / entityLabelColumn 변경 시
    // 옵션 재로드 되도록 명시. ref 는 객체 자체가 매번 바뀌면 effect 폭주하므로
    // 내부 키만 dep 으로 추출.
    config.entityTable,
    config.entityValueColumn,
    config.entityLabelColumn,
    config.ref?.table,
    config.ref?.valueColumn,
    config.ref?.displayColumn,
    // 필터 변경 시 URL 재생성 → cache key 자동 신규화
    resolvedFiltersJson,
    tableName,
    columnName,
  ]);

  const [fetched, setFetched] = useState<LoadedOption[] | null>(() => {
    return fetchUrl ? (responseCache.get(fetchUrl) ?? null) : null;
  });
  const [loading, setLoading] = useState(false);
  const reqIdRef = useRef(0);

  useEffect(() => {
    if (!fetchUrl) {
      // fetch 가 필요 없는 source / config 미충족 → 이전 fetch 의 loading 상태가
      // 남아있지 않도록 명시적으로 false. race guard 차원에서 reqId 도 +1.
      reqIdRef.current++;
      setFetched(null);
      setLoading(false);
      return;
    }
    const cached = responseCache.get(fetchUrl);
    if (cached) {
      // cache hit — 비동기 분기로 빠지지 않으므로 finally 가 안 돌아간다.
      // loading 이 true 인 상태에서 cache hit 가 발생하면 stuck 위험 → 직접 false.
      reqIdRef.current++;
      setFetched(cached);
      setLoading(false);
      return;
    }
    let cancelled = false;
    const myReq = ++reqIdRef.current;
    setLoading(true);
    apiClient
      .get(fetchUrl)
      .then((response) => {
        if (cancelled || myReq !== reqIdRef.current) return;
        const data = response?.data;
        let opts: LoadedOption[] = [];
        // 응답 shape 적응
        if (Array.isArray(data)) {
          opts = normalizeOptions(data);
        } else if (data && typeof data === "object") {
          if (data.success && Array.isArray(data.data)) {
            // category 트리 vs flat 분기
            if (source === "category") {
              opts = flattenCategoryTree(data.data as RawOptionItem[]);
            } else {
              opts = normalizeOptions(data.data);
            }
          } else if (Array.isArray((data as any).options)) {
            opts = normalizeOptions((data as any).options);
          }
        }
        responseCache.set(fetchUrl, opts);
        setFetched(opts);
      })
      .catch((err) => {
        if (cancelled || myReq !== reqIdRef.current) return;
        console.warn("[useOptionLoader] load failed:", fetchUrl, err);
        setFetched([]);
      })
      .finally(() => {
        if (cancelled || myReq !== reqIdRef.current) return;
        setLoading(false);
      });

    return () => {
      cancelled = true;
    };
  }, [fetchUrl, source]);

  // 최종 옵션 결정
  // - source = static : staticOptions
  // - source = api 등 외부 : fetched 가 null 이면 (아직 로딩 전) static fallback,
  //   fetched 가 있으면 fetched 우선
  // - isDesignMode 에서는 fetch 자체를 skip 하므로 staticOptions 만 사용
  const options = useMemo<LoadedOption[]>(() => {
    if (source === "static") return staticOptions;
    if (isDesignMode) return staticOptions;
    if (fetched == null) return staticOptions; // 첫 로드 전 — static 으로 silhouette
    return fetched;
  }, [source, isDesignMode, staticOptions, fetched]);

  return { options, loading };
}
