package com.booking.replication.applier.schema.registry;

import io.confluent.kafka.schemaregistry.client.SchemaMetadata;
import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient;
import io.confluent.kafka.schemaregistry.client.rest.RestService;
import io.confluent.kafka.schemaregistry.client.rest.entities.Config;
import io.confluent.kafka.schemaregistry.client.rest.entities.SchemaString;
import io.confluent.kafka.schemaregistry.client.rest.entities.requests.ConfigUpdateRequest;
import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException;
import io.confluent.kafka.schemaregistry.client.security.basicauth.BasicAuthCredentialProvider;
import io.confluent.kafka.schemaregistry.client.security.basicauth.BasicAuthCredentialProviderFactory;
import org.apache.avro.Schema;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;

// TODO: remove this and update to: https://github.com/confluentinc/schema-registry/pull/1009
// This class is identical to CachedSchemaRegistry except register method which uses hashmap instead of identityHashMap there by not recognising similar schemas
//        IdentityHashMap<Schema, String> myIhashmap = new IdentityHashMap<Schema, String>();
//        HashMap<Schema, String> myhashmap = new HashMap<Schema, String>();
//        mymap.put(schema, "one");
//        myhashmap.put(schema, "one");
//        Schema schema = new Schema.Parser().parse(SCHEMA_STRING);
//        Schema identicalschema = new Schema.Parser().parse(SCHEMA_STRING);

//        myIhashmap.containsKey(identicalschema) => false. Thereby marking it as cacheMiss.
//        myhashmap.containsKey(identicalschema) => true. Used in this version.

public class BCachedSchemaRegistryClient implements SchemaRegistryClient {
    private final RestService restService;
    private final int identityMapCapacity;
    private final Map<String, Map<Schema, Integer>> schemaCache;
    private final Map<String, Map<Integer, Schema>> idCache;
    private final Map<String, Map<Schema, Integer>> versionCache;
    public static final Map<String, String> DEFAULT_REQUEST_PROPERTIES = new HashMap();

    public BCachedSchemaRegistryClient(String baseUrl, int identityMapCapacity) {
        this(new RestService(baseUrl), identityMapCapacity);
    }

    public BCachedSchemaRegistryClient(List<String> baseUrls, int identityMapCapacity) {
        this(new RestService(baseUrls), identityMapCapacity);
    }

    public BCachedSchemaRegistryClient(RestService restService, int identityMapCapacity) {
        this((RestService)restService, identityMapCapacity, (Map)null);
    }

    public BCachedSchemaRegistryClient(String baseUrl, int identityMapCapacity, Map<String, ?> originals) {
        this(new RestService(baseUrl), identityMapCapacity, originals);
    }

    public BCachedSchemaRegistryClient(List<String> baseUrls, int identityMapCapacity, Map<String, ?> originals) {
        this(new RestService(baseUrls), identityMapCapacity, originals);
    }

    public BCachedSchemaRegistryClient(RestService restService, int identityMapCapacity, Map<String, ?> configs) {
        this.identityMapCapacity = identityMapCapacity;
        this.schemaCache = new HashMap<>();
        this.idCache = new HashMap<>();
        this.versionCache = new HashMap<>();
        this.restService = restService;
        this.idCache.put((String) null, new HashMap<>());
        this.configureRestService(configs);
    }

    private void configureRestService(Map<String, ?> configs) {
        if (configs != null) {
            String credentialSourceConfig = (String)configs.get("basic.auth.credentials.source");
            if (credentialSourceConfig != null && !credentialSourceConfig.isEmpty()) {
                BasicAuthCredentialProvider basicAuthCredentialProvider = BasicAuthCredentialProviderFactory.getBasicAuthCredentialProvider(credentialSourceConfig, configs);
                this.restService.setBasicAuthCredentialProvider(basicAuthCredentialProvider);
            }
        }
    }

    private int registerAndGetId(String subject, Schema schema) throws IOException, RestClientException {
        return this.restService.registerSchema(schema.toString(), subject);
    }

    private Schema getSchemaByIdFromRegistry(int id) throws IOException, RestClientException {
        SchemaString restSchema = this.restService.getId(id);
        return (new Schema.Parser()).parse(restSchema.getSchemaString());
    }

    private int getVersionFromRegistry(String subject, Schema schema) throws IOException, RestClientException {
        io.confluent.kafka.schemaregistry.client.rest.entities.Schema response = this.restService.lookUpSubjectVersion(schema.toString(), subject, true);
        return response.getVersion().intValue();
    }

    private int getIdFromRegistry(String subject, Schema schema) throws IOException, RestClientException {
        io.confluent.kafka.schemaregistry.client.rest.entities.Schema response = this.restService.lookUpSubjectVersion(schema.toString(), subject, false);
        return response.getId().intValue();
    }

    public synchronized int register(String subject, Schema schema) throws IOException, RestClientException {
        Map<Schema, Integer> schemaIdMap;
        if (this.schemaCache.containsKey(subject)) {
            schemaIdMap = (Map<Schema, Integer>) this.schemaCache.get(subject);
        } else {
            schemaIdMap = new HashMap<Schema, Integer>();
            this.schemaCache.put(subject, schemaIdMap);
        }

        if (((Map)schemaIdMap).containsKey(schema)) {
            return ((Integer)((Map)schemaIdMap).get(schema)).intValue();
        } else if (((Map)schemaIdMap).size() >= this.identityMapCapacity) {
            throw new IllegalStateException("Too many schema objects created for " + subject + "!");
        } else {
            int id = this.registerAndGetId(subject, schema);
            ((Map)schemaIdMap).put(schema, Integer.valueOf(id));
            ((Map)this.idCache.get((Object)null)).put(Integer.valueOf(id), schema);
            return id;
        }
    }

    public Schema getByID(int id) throws IOException, RestClientException {
        return this.getById(id);
    }

    public synchronized Schema getById(int id) throws IOException, RestClientException {
        return this.getBySubjectAndId((String)null, id);
    }

    public Schema getBySubjectAndID(String subject, int id) throws IOException, RestClientException {
        return this.getBySubjectAndId(subject, id);
    }

    public synchronized Schema getBySubjectAndId(String subject, int id) throws IOException, RestClientException {
        Map<Integer, Schema> idSchemaMap;
        if (this.idCache.containsKey(subject)) {
            idSchemaMap = (Map<Integer, Schema>)this.idCache.get(subject);
        } else {
            idSchemaMap = new HashMap<Integer, Schema>();
            this.idCache.put(subject, idSchemaMap);
        }

        if (idSchemaMap.containsKey(Integer.valueOf(id))) {
            return (Schema)((Map)idSchemaMap).get(Integer.valueOf(id));
        } else {
            Schema schema = this.getSchemaByIdFromRegistry(id);
            idSchemaMap.put(id, schema);
            return schema;
        }
    }

    public SchemaMetadata getSchemaMetadata(String subject, int version) throws IOException, RestClientException {
        io.confluent.kafka.schemaregistry.client.rest.entities.Schema response = this.restService.getVersion(subject, version);
        int id = response.getId().intValue();
        String schema = response.getSchema();
        return new SchemaMetadata(id, version, schema);
    }

    public synchronized SchemaMetadata getLatestSchemaMetadata(String subject) throws IOException, RestClientException {
        io.confluent.kafka.schemaregistry.client.rest.entities.Schema response = this.restService.getLatestVersion(subject);
        int id = response.getId().intValue();
        int version = response.getVersion().intValue();
        String schema = response.getSchema();
        return new SchemaMetadata(id, version, schema);
    }

    public synchronized int getVersion(String subject, Schema schema) throws IOException, RestClientException {
        Map<Schema, Integer> schemaVersionMap;
        if (this.versionCache.containsKey(subject)) {
            schemaVersionMap = (Map)this.versionCache.get(subject);
        } else {
            schemaVersionMap = new IdentityHashMap();
            this.versionCache.put(subject, schemaVersionMap);
        }

        if (((Map)schemaVersionMap).containsKey(schema)) {
            return ((Integer)((Map)schemaVersionMap).get(schema)).intValue();
        } else if (((Map)schemaVersionMap).size() >= this.identityMapCapacity) {
            throw new IllegalStateException("Too many schema objects created for " + subject + "!");
        } else {
            int version = this.getVersionFromRegistry(subject, schema);
            ((Map)schemaVersionMap).put(schema, Integer.valueOf(version));
            return version;
        }
    }

    public List<Integer> getAllVersions(String subject) throws IOException, RestClientException {
        return this.restService.getAllVersions(subject);
    }

    public synchronized int getId(String subject, Schema schema) throws IOException, RestClientException {
        Map<Schema, Integer> schemaIdMap;
        if (this.schemaCache.containsKey(subject)) {
            schemaIdMap = (Map)this.schemaCache.get(subject);
        } else {
            schemaIdMap = new HashMap<Schema, Integer>();
            this.schemaCache.put(subject, schemaIdMap);
        }

        if (((Map)schemaIdMap).containsKey(schema)) {
            return ((Integer)((Map)schemaIdMap).get(schema)).intValue();
        } else if (((Map)schemaIdMap).size() >= this.identityMapCapacity) {
            throw new IllegalStateException("Too many schema objects created for " + subject + "!");
        } else {
            int id = this.getIdFromRegistry(subject, schema);
            ((Map)schemaIdMap).put(schema, Integer.valueOf(id));
            ((Map)this.idCache.get((Object)null)).put(Integer.valueOf(id), schema);
            return id;
        }
    }

    public List<Integer> deleteSubject(String subject) throws IOException, RestClientException {
        return this.deleteSubject(DEFAULT_REQUEST_PROPERTIES, subject);
    }

    public List<Integer> deleteSubject(Map<String, String> requestProperties, String subject) throws IOException, RestClientException {
        this.versionCache.remove(subject);
        this.idCache.remove(subject);
        this.schemaCache.remove(subject);
        return this.restService.deleteSubject(requestProperties, subject);
    }

    public Integer deleteSchemaVersion(String subject, String version) throws IOException, RestClientException {
        return this.deleteSchemaVersion(DEFAULT_REQUEST_PROPERTIES, subject, version);
    }

    public Integer deleteSchemaVersion(Map<String, String> requestProperties, String subject, String version) throws IOException, RestClientException {
        ((Map)this.versionCache.get(subject)).values().remove(Integer.valueOf(version));
        return this.restService.deleteSchemaVersion(requestProperties, subject, version);
    }

    public boolean testCompatibility(String subject, Schema schema) throws IOException, RestClientException {
        return this.restService.testCompatibility(schema.toString(), subject, "latest");
    }

    public String updateCompatibility(String subject, String compatibility) throws IOException, RestClientException {
        ConfigUpdateRequest response = this.restService.updateCompatibility(compatibility, subject);
        return response.getCompatibilityLevel();
    }

    public String getCompatibility(String subject) throws IOException, RestClientException {
        Config response = this.restService.getConfig(subject);
        return response.getCompatibilityLevel();
    }

    public Collection<String> getAllSubjects() throws IOException, RestClientException {
        return this.restService.getAllSubjects();
    }

    static {
        DEFAULT_REQUEST_PROPERTIES.put("Content-Type", "application/vnd.schemaregistry.v1+json");
    }
}