import { RJSFSchema } from '@rjsf/utils';
import i18n from '../../../i18n'
import { JSONSchema7Definition } from "json-schema";

const API_URL = "/api/v1";

type CrudRJSFSchema = RJSFSchema & {
    properties?: {
        [key: string]: JSONSchema7Definition & {
          readOnly?: boolean | undefined;
        };
    } | undefined;
}

export const jsonschemaProvider = {
  getCardResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
    const updateSchema = await getResourceSchema(`${resourceName}Update`);
    const readSchema = await getResourceSchema(`${resourceName}Read`);

    for (let prop_name in readSchema.properties) {
        if (! updateSchema.hasOwnProperty(prop_name)) {
            if (is_reference(readSchema.properties[prop_name])) {
                let subresourceName = get_reference_name(readSchema.properties[prop_name]);
                readSchema.components.schemas[subresourceName].readOnly = true;
            } else {
                readSchema.properties[prop_name].readOnly = true;
            }
        }
    }
    changePropertiesOrder(readSchema);

    return readSchema
  },

  getUpdateResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
    return getResourceSchema(`${resourceName}Update`)
  },

  getCreateResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
    return getResourceSchema(`${resourceName}Create`)
  },
}

const getResourceSchema = async (resourceName: string): Promise<CrudRJSFSchema> => {
    return buildResource(await getJsonschema(), resourceName)
}

let rawSchema: RJSFSchema;
const getJsonschema = async (): Promise<RJSFSchema> => {
  if (rawSchema === undefined) {
    const response = await fetch(`${API_URL}/openapi.json`,);
    rawSchema = await response.json();
  }
  return rawSchema;
}

function convertCamelToSnake(str: string): string {
 return str.replace(/([a-zA-Z])(?=[A-Z])/g,'$1_').toLowerCase()
}

function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
  if (rawSchemas.components.schemas[resourceName] === undefined) {
    throw new Error(`Resource "${resourceName}" not found in schema.`);
  }

  const shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|Create|Update)$/g, ""));
  let resource = structuredClone(rawSchemas.components.schemas[resourceName]);
  resource.components = { schemas: {} };
  for (let prop_name in resource.properties) {
    let prop = resource.properties[prop_name];

    if (is_reference(prop)) {
      resolveReference(rawSchemas, resource, prop);
    } else if (is_union(prop)) {
      const union = prop.hasOwnProperty("oneOf") ? prop.oneOf : prop.anyOf;
      for (let i in union) {
        if (is_reference(union[i])) {
          resolveReference(rawSchemas, resource, union[i]);
        }
      }
    } else if (is_enum(prop)) {
      for (let i in prop.allOf) {
        if (is_reference(prop.allOf[i])) {
          resolveReference(rawSchemas, resource, prop.allOf[i]);
        }
      }
    } else if (is_array(prop) && is_reference(prop.items)) {
      resolveReference(rawSchemas, resource, prop.items);
    }

    if (prop.hasOwnProperty("title")) {
      prop.title = i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prop_name)}`, prop.title);
    }
  }
  if (resource.hasOwnProperty("title")) {
    resource.title = i18n.t(`schemas.${shortResourceName}.resource_title`, resource.title);
  }

  return resource;
}

function resolveReference(rawSchemas: RJSFSchema, resource: any, prop_reference: any) {
  const subresourceName = get_reference_name(prop_reference);
  const subresource = buildResource(rawSchemas, subresourceName);
  resource.components.schemas[subresourceName] = subresource;
  for (let subsubresourceName in subresource.components.schemas) {
    if (! resource.components.schemas.hasOwnProperty(subsubresourceName)) {
      resource.components.schemas[subsubresourceName] = subresource.components.schemas[subsubresourceName];
    }
  }
}

function changePropertiesOrder(resource: any) {
  let created_at;
  let created_by;
  let updated_at;
  let updated_by;
  let new_properties: any = {};
  for (let prop_name in resource.properties) {
    if (prop_name == 'created_at') {
      created_at = resource.properties[prop_name];
    } else if (prop_name == 'created_by') {
      created_by = resource.properties[prop_name];
    } else if (prop_name == 'updated_at') {
      updated_at = resource.properties[prop_name];
    }  else if (prop_name == 'updated_by') {
      updated_by = resource.properties[prop_name];
    }else {
      new_properties[prop_name] = resource.properties[prop_name];
    }
  }
  if (created_at) {
    new_properties['created_at'] = created_at;
  }
  if (created_by) {
    new_properties['created_by'] = created_by;
  }
  if (updated_at) {
    new_properties['updated_at'] = updated_at;
  }
  if (updated_by) {
    new_properties['updated_by'] = updated_by;
  }
  resource.properties = new_properties
}

function is_object(prop: any) {
  return prop.hasOwnProperty('properties')
}

function is_reference(prop: any) {
  return prop.hasOwnProperty('$ref');
}

function is_array(prop: any) {
  return prop.hasOwnProperty('items');
}

function is_union(prop: any) {
  return prop.hasOwnProperty('oneOf') || prop.hasOwnProperty('anyOf');
}

function is_enum(prop: any) {
  return prop.hasOwnProperty('enum');
}

function get_reference_name(prop: any) {
  return prop['$ref'].substring(prop['$ref'].lastIndexOf('/')+1);
}

function has_descendant(rawSchemas:RJSFSchema, resource: RJSFSchema, property_name: string): boolean {
  if (is_array(resource)) {
    return property_name == 'items';
  } else if (is_object(resource)) {
    return property_name in resource.properties!;
  } else if (is_reference(resource)) {
    let subresourceName = get_reference_name(resource);
    return has_descendant(rawSchemas, buildResource(rawSchemas, subresourceName), property_name);
  } else if (is_union(resource)) {
    const union = resource.hasOwnProperty("oneOf") ? resource.oneOf : resource.anyOf;
    if (union !== undefined) {
      for (const ref of union) {
        return has_descendant(rawSchemas, ref as RJSFSchema, property_name)
      }
    }
  } else if (is_enum(resource)) {
    for (const ref of resource.allOf!) {
      return has_descendant(rawSchemas, ref as RJSFSchema, property_name);
    }
  }
  throw new Error("Jsonschema format not implemented in property finder");
}

function get_descendant(rawSchemas: RJSFSchema, resource: RJSFSchema, property_name: string): RJSFSchema {
  if (is_array(resource) && property_name == 'items') {
    return resource.items as RJSFSchema;
  } else if (is_object(resource) && resource.properties !== undefined && property_name in resource.properties!) {
    return resource.properties[property_name] as RJSFSchema;
  } else if (is_reference(resource)) {
    let subresourceName = get_reference_name(resource);
    let subresource = buildResource(rawSchemas, subresourceName);
    return get_descendant(rawSchemas, subresource, property_name);
  } else if (is_union(resource)) {
    for (const ref of resource.oneOf!) {
      if (has_descendant(rawSchemas, ref as RJSFSchema, property_name)) {
        return get_descendant(rawSchemas, ref as RJSFSchema, property_name);
      }
    }
  } else if (is_enum(resource)) {
      for (const ref of resource.allOf!) {
          if (has_descendant(rawSchemas, ref as RJSFSchema, property_name)) {
              return get_descendant(rawSchemas, ref as RJSFSchema, property_name);
          }
      }
  }
  throw new Error("property not found or Jsonschema format not implemented");
}

function path_exists(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string): boolean{
  const pointFirstPosition = path.indexOf('.')
  if (pointFirstPosition == -1) {
    return has_descendant(rawSchemas, resource, path);
  }

  return has_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition))
    && path_exists(
      rawSchemas,
      get_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition)),
      path.substring(pointFirstPosition + 1)
    );
}

function get_property_by_path(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string): RJSFSchema {
  const pointFirstPosition = path.indexOf('.')
  if (pointFirstPosition == -1) {
    return get_descendant(rawSchemas, resource, path);
  }

  return get_property_by_path(
    rawSchemas,
    get_descendant(
      rawSchemas,
      resource,
      path.substring(0, pointFirstPosition)
    ),
    path.substring(pointFirstPosition + 1)
  );
}
