import { Observable } from "rxjs";
import { Injectable } from "@angular/core";
import { ApiService } from "./crud.service";
import {JSONSchema7} from "json-schema";


@Injectable({
  providedIn: 'root',
})
export class JsonschemasService {
    private rawSchemas: any | null = null;

    constructor(
        private apiService: ApiService
    ) {}

    getSchemas(): Observable<Schema> {
        return new Observable<Schema>((observer) => {
            if (this.rawSchemas === null) {
                this.apiService.getSchema().subscribe((jsonSchemas: any) => {
                    this.rawSchemas = jsonSchemas;
                    observer.next(this.rawSchemas)
                });
            } else {
                observer.next(this.rawSchemas)
            }
        });
    }

    buildResource(resourceName: string) {
        let resource;

        resource = structuredClone(this.rawSchemas.components.schemas[resourceName]);
        resource.components = { schemas: {} };
        for (let prop_name in resource.properties) {
            let prop = resource.properties[prop_name];

            if (prop_name === '_id') {
                delete resource.properties[prop_name]
            } else if (this.is_reference(prop)) {
                this.resolveReference(resource, prop);
            } else if (this.is_union(prop)) {
                for (let i in prop.oneOf) {
                    this.resolveReference(resource, prop.oneOf[i]);
                }
            } else if (this.is_enum(prop)) {
                for (let i in prop.allOf) {
                    this.resolveReference(resource, prop.allOf[i]);
                }
            } else if (this.is_array(prop) && this.is_reference(prop.items)) {
                this.resolveReference(resource, prop.items);
            }
        }

        return resource;
    }

    resolveReference(resource: any, prop_reference: any) {
        let subresourceName = this.get_reference_name(prop_reference);
        let subresource = this.buildResource(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];
            }
        }
    }

    getReferencedSchema() {

    }

    getCreateResource(resourceName: string): Observable<Schema> {
        return new Observable<Schema>((observer) => {
            this.getSchemas().subscribe(() => {
                observer.next(this.buildResource(resourceName + 'Create'))
            })
        })
    }

    getUpdateResource(resourceName: string): Observable<Schema> {
        return new Observable<Schema>((observer) => {
            this.getSchemas().subscribe(() => {
                let resource = this.buildResource(resourceName + 'Read');
                let updateResource = this.rawSchemas.components.schemas[resourceName + 'Update']

                for (let prop_name in resource.properties) {
                    if (! updateResource.properties.hasOwnProperty(prop_name)) {
                        if (this.is_reference(resource.properties[prop_name])) {
                            let subresourceName = this.get_reference_name(resource.properties[prop_name]);
                            resource.components.schemas[subresourceName].readOnly = true;
                        } else {
                            resource.properties[prop_name].readOnly = true;
                        }
                    }
                }
                this.changePropertiesOrder(resource);
                observer.next(resource);
            })
        })
    }

    changePropertiesOrder(resource: any) {
        let created_at;
        let updated_at;
        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 == 'updated_at') {
                updated_at = resource.properties[prop_name];
            } else {
                new_properties[prop_name] = resource.properties[prop_name];
            }
        }
        if (created_at) {
            new_properties['created_at'] = created_at;
        }
        if (updated_at) {
            new_properties['updated_at'] = updated_at;
        }
        resource.properties = new_properties
    }

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

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

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

    private is_union(prop: any) {
        return prop.hasOwnProperty('oneOf');
    }

    private is_enum(prop: any) {
        return prop.hasOwnProperty('allOf');
    }

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

    has_descendant(resource: JSONSchema7, property_name: string): boolean {
        if (this.is_array(resource)) {
            return property_name == 'items';
        } else if (this.is_object(resource)) {
            return property_name in resource.properties!;
        } else if (this.is_reference(resource)) {
            let subresourceName = this.get_reference_name(resource);
            return this.has_descendant(this.buildResource(subresourceName), property_name);
        } else if (this.is_union(resource)) {
            for (const ref of resource.oneOf!) {
                // @ts-ignore
                if (this.has_descendant(ref, property_name)) {
                    return true;
                }
                return false;
            }
        } else if (this.is_enum(resource)) {
            for (const ref of resource.allOf!) {
                // @ts-ignore
                if (this.has_descendant(ref, property_name)) {
                    return true;
                }
                return false;
            }
        }
        throw new Error("Jsonschema format not implemented in property finder");
        return false;
    }

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

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


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

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

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

export interface Schema {
  components: { };
}
