import { useState, useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import type { Dispatch } from '@reduxjs/toolkit';
import { CmdbItem, getItem } from '../store/cmdbSlice/thunks';

export const MAX_NAME_LENGTH = 255;
export const MAX_VALUE_LENGTH = 16000;
export const NAME_PATTERN = /^[a-zA-Z0-9_.:]+$/;
export const ATTRIBUTE_NAME_PATTERN = /^[a-zA-Z0-9_ ]+$/;
export const VARIABLE_PATTERN = /[$@][({}].*[){}]/g;
export const ALPHANUMERIC_PATTERN = /^[a-zA-Z0-9 ]+$/;

type ValidationError = string | null;
type AsyncValidator<T = unknown> = (value: T, context: ValidationContext) => Promise<ValidationError>;
type SyncValidator<T = unknown> = (value: T, context: ValidationContext) => ValidationError;
type Validator<T = unknown> = SyncValidator<T> | AsyncValidator<T>;

interface ValidationContext {
  fieldName: string;
  identifier?: string;
  entry_id?: number;
  type?: string;
  dispatch?: Dispatch;
}

interface ValidationCallbackParams {
  item: CmdbItem;
  field: string;
  fieldRules: FieldRule;
  identifier?: string;
  type?: string;
  dispatch: Dispatch;
}

type ValidationErrors = Record<string, string>;

interface FieldRule {
  required?: boolean;
  maxLength?: number;
  pattern?: RegExp | string;
  patternMessage?: string;
  checkExists?: boolean;
  noVariables?: boolean;
  isArray?: boolean;
  validJSON?: boolean;
  alphaNumeric?: boolean;
}

interface ValidationRules {
  [fieldName: string]: FieldRule;
}

interface ValidateParams {
  item: CmdbItem;
  identifier?: string;
  type?: string;
  customValidations?: Record<string, Validator>;
}

interface UseValidationReturn {
  errors: Record<string, string>;
  validate: (params: ValidateParams) => Promise<boolean>;
  clearErrors: () => void;
  setFieldError: (field: string, error: string) => void;
  setErrors: React.Dispatch<React.SetStateAction<Record<string, string>>>;
}

export const defaultRules: ValidationRules = {
  name: {
    maxLength: MAX_NAME_LENGTH,
  },
  description: {
    maxLength: MAX_VALUE_LENGTH,
    noVariables: true
  },
  tags: {
    maxLength: MAX_VALUE_LENGTH,
    noVariables: true,
    isArray: true
  }
};

export const defaultSubentriesRules: ValidationRules = {
  item_name: {
    required: true,
    maxLength: MAX_NAME_LENGTH,
    pattern: NAME_PATTERN,
    patternMessage: "Name can only contain letters, numbers, underscores, dots, and colons",
    checkExists: true
  },
  item_value: {
    maxLength: MAX_VALUE_LENGTH
  }
};

const validators = {
  required: (value: unknown, { fieldName }: ValidationContext): ValidationError => {
    const isEmpty = !value ||
      (typeof value === 'string' && !value.trim()) ||
      (Array.isArray(value) && value.length === 0);

    return isEmpty ? `${fieldName} is required` : null;
  },

  maxLength: (value: string | unknown[], { fieldName }: ValidationContext, maxLength: number): ValidationError => {
    const length = typeof value === 'string' ? value.length : (value as unknown[])?.length || 0;
    return length > maxLength ? `${fieldName} must be ${maxLength} characters or less` : null;
  },

  validJSON: (value: unknown, { fieldName }: ValidationContext): ValidationError => {
    if (typeof value !== 'string') return null;
    
    try {
      JSON.parse(value);
      return null;
    } catch (error) {
      return `${fieldName} is invalid JSON. ${error.message}`;
    }
  },

  pattern: (
    value: string,
    { fieldName }: ValidationContext,
    pattern: RegExp | string,
    patternMessage?: string
  ): ValidationError => {
    if (!value) return null;

    const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
    return !regex.test(value)
      ? patternMessage || `${fieldName} format is invalid`
      : null;
  },

  noVariables: (value: string, { fieldName }: ValidationContext): ValidationError => {
    return value && VARIABLE_PATTERN.test(value)
      ? "CFEngine variables are not allowed"
      : null;
  },

  alphaNumeric: (value: string): ValidationError => {
    return value && ALPHANUMERIC_PATTERN.test(value)
      ? null
      : "Value should be alphanumeric";
  },

  arrayMaxLength: (array: unknown[], context: ValidationContext, maxLength: number): ValidationError => {
    if (!Array.isArray(array)) return null;
    const joinedValue = array.join('');
    return validators.maxLength(joinedValue, context, maxLength);
  },

  arrayNoVariables: (array: unknown[]): ValidationError => {
    if (!Array.isArray(array)) return null;
    const hasVariables = array.some(item =>
      typeof item === 'string' && VARIABLE_PATTERN.test(item)
    );
    return hasVariables ? "CFEngine variables are not allowed" : null;
  },

  uniqueName: async (
    value: string,
    { identifier, type, entry_id, dispatch }: ValidationContext
  ): Promise<ValidationError> => {
    if (!value || !identifier || !type || !dispatch) return null;
    try {
      const response = await dispatch(getItem({ identifier, type, name: value })).unwrap();
      // skip if entry id is the same which means we need to update the item
      if (entry_id && response?.entry_id === entry_id) {
        return null;
      }
      // successful response means the name exists (error means it doesn't)
      return "Name already exists";
    } catch {
      return null;
    }
  }
};

/**
 * Formats field name from camelCase to human-readable format
 */
const formatFieldName = (fieldName: string): string => {
  return fieldName
    .replace(/([A-Z])/g, ' $1')
    .replace(/^./, (s: string) => s.toUpperCase());
};


const validationCallback = async ({
  item,
  field,
  fieldRules,
  identifier,
  type,
  dispatch
}: ValidationCallbackParams): Promise<ValidationErrors> => {
  const value = item[field as keyof CmdbItem];
  const fieldName = item?.error_field ?? field;
  const context: ValidationContext = {
    fieldName: formatFieldName(fieldName),
    identifier,
    type,
    dispatch,
    entry_id: item?.entry_id
  };
  const errors: ValidationErrors = {};

  if (fieldRules.isArray && Array.isArray(value)) {
    if (fieldRules.maxLength) {
      const error = validators.arrayMaxLength(value, context, fieldRules.maxLength);
      if (error) {
        errors[fieldName] = error;
      }
    }

    if (fieldRules.noVariables) {
      const error = validators.arrayNoVariables(value);
      if (error) {
        errors[fieldName] = error;
      }
    }
  } else {
    if (fieldRules.required) {
      const error = validators.required(value, context);
      if (error) {
        errors[fieldName] = error;
      }
    }

    if (fieldRules.checkExists) {
      const error = await validators.uniqueName(value as string, context);
      if (error) {
        errors[fieldName] = error;
      }
    }

    if (fieldRules.maxLength && typeof value === 'string') {
      const error = validators.maxLength(value, context, fieldRules.maxLength);
      if (error) {
        errors[fieldName] = error;
      }
    }

    if (fieldRules.validJSON) {
      const error = validators.validJSON(value, context);
      if (error) {
        errors[fieldName] = error;
      }
    }

    if (fieldRules.pattern && typeof value === 'string') {
      const error = validators.pattern(
        value,
        context,
        fieldRules.pattern,
        fieldRules.patternMessage
      );
      if (error) {
        errors[fieldName] = error;
      }
    }

    if (fieldRules.noVariables && typeof value === 'string') {
      const error = validators.noVariables(value, context);
      if (error) {
        errors[fieldName] = error;
      }
    }

    if (fieldRules.alphaNumeric && typeof value === 'string') {
      const error = validators.alphaNumeric(value);
      if (error) {
        errors[fieldName] = error;
      }
    }
  }

  return errors;
};

const useValidation = ({ customRules = {}, customSubentryRules = {} } = {}): UseValidationReturn => {
  const [errors, setErrors] = useState<Record<string, string>>({});
  const dispatch = useDispatch();

  const rules = useMemo(
    () => ({ ...defaultRules, ...customRules }),
    [customRules]
  );

  const subEntryRules = useMemo(
    () => ({ ...defaultSubentriesRules, ...customSubentryRules }),
    [customSubentryRules]
  );

  const validate = useCallback(async ({
    item,
    identifier
  }: ValidateParams): Promise<boolean> => {

    const validations = [
      Object.entries(rules).map(async ([field, fieldRules]) =>
        await validationCallback({ item, identifier, field, fieldRules, dispatch })
      ),
      ...item.entries.map(item => Object.entries(subEntryRules).map(async ([field, fieldRules]) =>
        await validationCallback({ item, identifier, field, type: item.item_type, fieldRules, dispatch })
      ))
    ];

    const validationErrors = (await Promise.all(validations.flat())).reduce((acc, obj) => Object.assign(acc, obj), {});
    setErrors(validationErrors);

    return Object.keys(validationErrors).length === 0;
  }, [rules, dispatch]);

  const clearErrors = useCallback(() => {
    setErrors({});
  }, []);

  const setFieldError = useCallback((field: string, error: string) => {
    setErrors(prev => ({ ...prev, [field]: error }));
  }, []);

  return {
    errors,
    validate,
    clearErrors,
    setFieldError,
    setErrors
  };
};

export default useValidation;
