import { useState, useCallback, useMemo, useEffect, useRef } from "react";
import { Autocomplete, TextField } from "@mui/material";
import type { ResourceConfig, ParsedListResponse, FieldConfig } from "../types";
import { useAppContext } from "./AppContext";
import { getApi } from "../hooks/useApi";
import { StringField } from "../components/fields/renderers/StringField";
import { NumberField } from "../components/fields/renderers/NumberField";
import { DateField } from "../components/fields/renderers/DateField";
import { BooleanField } from "../components/fields/renderers/BooleanField";
import { EnumField } from "../components/fields/renderers/EnumField";
import { FkSelectField } from "../components/fields/renderers/FkSelectField";
import { FkMultiSelectField } from "../components/fields/renderers/FkMultiSelectField";

function parseError(e: any): string {
  if (e.response?.data) {
    const data = e.response.data;
    if (Array.isArray(data)) {
      return data.map((err: any) => err.msg ?? String(err)).join("; ");
    }
    if (typeof data.detail === "string") {
      return data.detail;
    }
  }
  return e.message ?? "An error occurred";
}

interface ResourceState {
  loading: boolean;
  error: string | null;
}

export interface FilterComponentProps {
  value: string;
  onChange: (v: string) => void;
  data?: any[];
  labelOverride?: string;
}

interface StreamHandlers {
  onEvent: (data: any) => void;
  onError?: (evt: Event) => void;
  onOpen?: () => void;
}

interface StreamSubscription {
  close: () => void;
}

interface UseResourceReturn {
  resource: ResourceConfig;
  components: Record<string, React.FC<FilterComponentProps>>;
  list: (params?: Record<string, any>) => Promise<ParsedListResponse>;
  get: (id: string | number) => Promise<any>;
  create: (data: any) => Promise<any>;
  update: (id: string | number, data: any) => Promise<any>;
  remove: (id: string | number) => Promise<void>;
  stream?: (handlers: StreamHandlers) => StreamSubscription;
  loading: boolean;
  error: string | null;
}

const _fkOptionsCache = new Map<string, { value: any; label: string }[]>();
const _stringOptionsCache = new Map<string, string[]>();
const _sseEventCache = new Map<string, any[]>();
let _sseSeq = 0;

export function readSseCache(resourceName: string): any[] {
  return _sseEventCache.get(resourceName) ?? [];
}

export function appendSseCache(resourceName: string, event: any): any[] {
  const events = _sseEventCache.get(resourceName) ?? [];
  events.push(event);
  if (events.length > 100) events.splice(0, events.length - 100);
  _sseEventCache.set(resourceName, events);
  return events;
}

export function clearSseCache(resourceName: string): void {
  _sseEventCache.delete(resourceName);
}

export function nextSseSeq(): number {
  return ++_sseSeq;
}

const _sseConnection = new Map<string, boolean>();
const _sseListeners = new Map<string, Set<() => void>>();

export function setSseConnected(resourceName: string, connected: boolean): void {
  if (_sseConnection.get(resourceName) === connected) return;
  _sseConnection.set(resourceName, connected);
  _sseListeners.get(resourceName)?.forEach((cb) => cb());
}

export function getSseConnected(resourceName: string): boolean {
  return _sseConnection.get(resourceName) ?? false;
}

export function useSseConnected(resourceName: string): boolean {
  const [connected, setConnected] = useState(() => getSseConnected(resourceName));

  useEffect(() => {
    const cb = () => setConnected(getSseConnected(resourceName));
    const listeners = _sseListeners.get(resourceName) ?? new Set();
    listeners.add(cb);
    _sseListeners.set(resourceName, listeners);
    return () => {
      listeners.delete(cb);
      if (listeners.size === 0) _sseListeners.delete(resourceName);
    };
  }, [resourceName]);

  return connected;
}

function extractDataOptions(data: any[], fieldName: string): string[] {
  const values = new Set<string>();
  for (const row of data) {
    const v = row[fieldName];
    if (v != null && v !== "") {
      values.add(String(v));
    }
  }
  return [...values].sort();
}

function applyDisplayFormat(obj: any, format: string): string {
  if (!obj || typeof obj !== "object") return String(obj ?? "");
  return format.replace(/\{(\w+)\}/g, (_, key) => String(obj[key] ?? ""));
}

function buildFilterComponent(field: FieldConfig, resourceName: string): React.FC<FilterComponentProps> {
  if (field.type === "boolean") {
    return ({ value, onChange, labelOverride }) => (
      <BooleanField
        field={{ ...field, readOnly: false, description: "", label: labelOverride ?? field.label }}
        value={value}
        onChange={(v) => onChange(v ?? "")}
        nullable
      />
    );
  }

  if (field.fk) {
    const FkFilter: React.FC<FilterComponentProps> = ({ value, onChange, data, labelOverride }) => {
      const { resources, config } = useAppContext();
      const filterMode = config.resourceConfig?.[resourceName]?.filterOptions?.mode ?? "server";
      const targetRes = resources.find((r) => r.name === field.fk!.resource);
      const [options, setOptions] = useState<{ value: any; label: string }[]>([]);
      const fetched = useRef(false);

      useEffect(() => {
        if (filterMode === "client" && data && targetRes) {
          const seen = new Set<string>();
          const opts: { value: any; label: string }[] = [];
          for (const row of data) {
            const items = Array.isArray(row[field.name]) ? row[field.name] : [row[field.name]];
            for (const item of items) {
              if (item == null) continue;
              const label = applyDisplayFormat(item, targetRes.displayFormat);
              if (!seen.has(label)) {
                seen.add(label);
                opts.push({ value: label, label });
              }
            }
          }
          opts.sort((a, b) => a.label.localeCompare(b.label));
          setOptions(opts);
          fetched.current = true;
        } else if (filterMode === "server" && targetRes && !fetched.current) {
          const cacheKey = targetRes.name;
          if (_fkOptionsCache.has(cacheKey)) {
            setOptions(_fkOptionsCache.get(cacheKey)!);
            fetched.current = true;
          } else {
            (async () => {
              try {
                const api = getApi();
                const params: Record<string, any> = {};
                if (targetRes.pagination) params.limit = 0;
                const res = await api.get(targetRes.path, { params });
                let items: any[];
                if (targetRes.pagination) {
                  items = res.data.items ?? [];
                } else {
                  items = Array.isArray(res.data) ? res.data : [];
                }
                const opts = items.map((item: any) => {
                  const label = applyDisplayFormat(item, targetRes.displayFormat);
                  return { value: label, label };
                });
                _fkOptionsCache.set(cacheKey, opts);
                setOptions(opts);
                fetched.current = true;
              } catch {
                fetched.current = true;
              }
            })();
          }
        }
      }, [filterMode, data, targetRes]);

      if (field.isArray) {
        const selected = value ? value.split(",").filter(Boolean) : [];
        return (
          <FkMultiSelectField
            field={{ ...field, readOnly: false, description: "", label: labelOverride ?? field.label }}
            value={selected}
            onChange={(v: any[]) => onChange(v.join(","))}
            fkOptions={options}
          />
        );
      }

      return (
        <FkSelectField
          field={{ ...field, readOnly: false, description: "", label: labelOverride ?? field.label }}
          value={value}
          onChange={(v: any) => onChange(v ?? "")}
          fkOptions={options}
        />
      );
    };
    return FkFilter;
  }

  if (field.enumValues) {
    const EnumFilter: React.FC<FilterComponentProps> = ({ value, onChange, data, labelOverride }) => {
      const dataOptions = useMemo(() => {
        if (!data) return [];
        return extractDataOptions(data, field.name);
      }, [data]);
      const merged = useMemo(
        () => [...new Set([...(field.enumValues ?? []), ...dataOptions])],
        [dataOptions]
      );
      return (
        <EnumField
          field={{ ...field, readOnly: false, description: "", label: labelOverride ?? field.label, enumValues: merged }}
          value={value}
          onChange={(v) => onChange(v ?? "")}
        />
      );
    };
    return EnumFilter;
  }

  if (field.type === "integer" || field.type === "number") {
    return ({ value, onChange, labelOverride }) => (
      <NumberField
        field={{ ...field, readOnly: false, description: "", label: labelOverride ?? field.label }}
        value={value}
        onChange={(v) => onChange(v === "" ? "" : String(v))}
      />
    );
  }

  if (field.format === "date" || field.format === "date-time") {
    return ({ value, onChange, labelOverride }) => (
      <DateField
        field={{ ...field, readOnly: false, description: "", label: labelOverride ?? field.label }}
        value={value}
        onChange={(v) => onChange(v ?? "")}
      />
    );
  }

  if (
    !field.fk &&
    !field.enumValues &&
    field.type !== "boolean" &&
    field.type !== "integer" &&
    field.type !== "number" &&
    field.format !== "date" &&
    field.format !== "date-time" &&
    !field.refSchema
  ) {
    const StringAutocompleteFilter: React.FC<FilterComponentProps> = ({ value, onChange, data, labelOverride }) => {
      const { resources, config } = useAppContext();
      const filterMode = config.resourceConfig?.[resourceName]?.filterOptions?.mode ?? "server";
      const [options, setOptions] = useState<string[]>([]);
      const fetched = useRef(false);

      useEffect(() => {
        if (filterMode === "client" && data) {
          setOptions(extractDataOptions(data, field.name));
          fetched.current = true;
        } else if (filterMode === "server" && !fetched.current) {
          const cacheKey = resourceName + ":" + field.name;
          if (_stringOptionsCache.has(cacheKey)) {
            setOptions(_stringOptionsCache.get(cacheKey)!);
            fetched.current = true;
          } else {
            (async () => {
              try {
                const api = getApi();
                const selfRes = resources.find((r) => r.name === resourceName);
                if (!selfRes) { fetched.current = true; return; }
                const params: Record<string, any> = {};
                if (selfRes.pagination) params.limit = 0;
                const res = await api.get(selfRes.path, { params });
                let items: any[];
                if (selfRes.pagination) {
                  items = res.data.items ?? [];
                } else {
                  items = Array.isArray(res.data) ? res.data : [];
                }
                const values = [...new Set(items.map((r: any) => String(r[field.name] ?? "")).filter(Boolean))].sort();
                _stringOptionsCache.set(cacheKey, values);
                setOptions(values);
                fetched.current = true;
              } catch {
                fetched.current = true;
              }
            })();
          }
        }
      }, [data]);

      return (
        <Autocomplete
          freeSolo
          size="small"
          options={options}
          value={value || null}
          onInputChange={(_, newVal) => onChange(newVal ?? "")}
          renderInput={(params) => (
            <TextField
              {...params}
              label={labelOverride ?? field.label}
              size="small"
            />
          )}
        />
      );
    };
    return StringAutocompleteFilter;
  }

  return ({ value, onChange, labelOverride }) => (
    <StringField
      field={{ ...field, readOnly: false, description: "", label: labelOverride ?? field.label }}
      value={value}
      onChange={(v) => onChange(v ?? "")}
    />
  );
}

export function useResource(resourceName: string): UseResourceReturn {
  const { resources } = useAppContext();
  const resource = resources.find((r) => r.name === resourceName);

  const [state, setState] = useState<ResourceState>({ loading: false, error: null });

  if (!resource) {
    const noop = async () => { throw new Error(`Resource "${resourceName}" not found yet`); };
    return {
      resource: null as unknown as ResourceConfig,
      components: {},
      list: noop,
      get: noop,
      create: noop,
      update: noop,
      remove: noop,
      loading: false,
      error: null,
    };
  }

  const setLoading = useCallback((loading: boolean) => {
    setState((s) => ({ ...s, loading }));
  }, []);

  const setError = useCallback((error: string | null) => {
    setState((s) => ({ ...s, error }));
  }, []);

  const list = useCallback(
    async (params?: Record<string, any>): Promise<ParsedListResponse> => {
      setLoading(true);
      setError(null);
      try {
        const api = getApi();
        const res = await api.get(resource.path, { params });
        const data = res.data;

        if (resource.pagination) {
          if (!data || typeof data !== "object" || !Array.isArray(data.items)) {
            throw new Error(`Expected paginated response { total, items } from ${resource.path}`);
          }
          return { items: data.items, total: data.total ?? data.items.length };
        }

        if (!Array.isArray(data)) {
          throw new Error(`Expected array response from ${resource.path}`);
        }
        return { items: data };
      } catch (e: any) {
        const msg = parseError(e);
        setError(msg);
        return { items: [] };
      } finally {
        setLoading(false);
      }
    },
    [resource.path, resource.pagination, setLoading, setError]
  );

  const get = useCallback(
    async (id: string | number): Promise<any> => {
      setLoading(true);
      setError(null);
      try {
        const api = getApi();
        const res = await api.get(`${resource.path}/${id}`);
        return res.data;
      } catch (e: any) {
        setError(parseError(e));
        throw e;
      } finally {
        setLoading(false);
      }
    },
    [resource.path, setLoading, setError]
  );

  const create = useCallback(
    async (data: any): Promise<any> => {
      setLoading(true);
      setError(null);
      try {
        const api = getApi();
        const res = await api.post(resource.path, data);
        return res.data;
      } catch (e: any) {
        setError(parseError(e));
        throw e;
      } finally {
        setLoading(false);
      }
    },
    [resource.path, setLoading, setError]
  );

  const update = useCallback(
    async (id: string | number, data: any): Promise<any> => {
      setLoading(true);
      setError(null);
      try {
        const api = getApi();
        const method = resource.updateMethod ?? "put";
        const res = await (method === "patch" ? api.patch : api.put)(`${resource.path}/${id}`, data);
        return res.data;
      } catch (e: any) {
        setError(parseError(e));
        throw e;
      } finally {
        setLoading(false);
      }
    },
    [resource.path, resource.updateMethod, setLoading, setError]
  );

  const remove = useCallback(
    async (id: string | number): Promise<void> => {
      setLoading(true);
      setError(null);
      try {
        const api = getApi();
        await api.delete(`${resource.path}/${id}`);
      } catch (e: any) {
        setError(parseError(e));
        throw e;
      } finally {
        setLoading(false);
      }
    },
    [resource.path, setLoading, setError]
  );

  const stream = useCallback(
    (handlers: StreamHandlers): StreamSubscription => {
      if (!resource.streaming) {
        throw new Error(`Resource "${resourceName}" does not support streaming`);
      }
      const api = getApi();
      const baseUrl = (api.defaults.baseURL ?? "").replace(/\/+$/, "");
      const url = baseUrl + resource.path;
      const es = new EventSource(url);

      es.onopen = () => handlers.onOpen?.();
      es.onmessage = (e) => {
        try {
          const data = JSON.parse(e.data);
          handlers.onEvent(data);
        } catch {
          // ignore malformed JSON payloads
        }
      };
      es.onerror = (e) => {
        handlers.onError?.(e);
      };

      return { close: () => es.close() };
    },
    [resource.path, resource.streaming, resourceName]
  );

  const components = useMemo(
    () => {
      const map: Record<string, React.FC<FilterComponentProps>> = {};
      for (const field of resource.fields) {
        map[field.name] = buildFilterComponent(field, resourceName);
      }
      return map;
    },
    [resource.fields, resourceName]
  );

  return { resource, components, list, get, create, update, remove, stream: resource.streaming ? stream : undefined, loading: state.loading, error: state.error };
}