package cn.ibizlab.core.data.service;

import cn.ibizlab.core.data.domain.DOModel;
import cn.ibizlab.core.data.lite.*;
import cn.ibizlab.core.data.model.DSLink;
import cn.ibizlab.core.data.model.POSchema;
import cn.ibizlab.core.data.model.PojoSchema;
import cn.ibizlab.core.data.model.TransUtils;
import cn.ibizlab.core.data.service.impl.DbPersistentServiceImpl;
import cn.ibizlab.core.data.service.impl.EsPersistentServiceImpl;
import cn.ibizlab.core.data.service.impl.MongoPersistentServiceImpl;
import cn.ibizlab.util.errors.BadRequestAlertException;
import cn.ibizlab.util.helper.DataObject;
import cn.ibizlab.util.helper.Inflector;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.sql.Timestamp;
import java.util.*;

@Service
@Slf4j
public class ModelService {

    public static String MODEL_PATH;

    @Value("${ibiz.model.path:/app/file/model/}")
    private String modelPath;

    public String getModelPath()
    {
        if(modelPath.equals(File.separator))
            return modelPath.substring(0,modelPath.length()-1);
        return modelPath;
    }

    @Autowired
    private IDOModelService doModelService;

    @Autowired
    private LiteModelFeignClient liteService1;

    @Autowired
    @Lazy
    private DynamicModelService dynamicService;

    @Autowired
    @Lazy
    private ModelService proxy;

    @Autowired
    @Lazy
    private DbPersistentServiceImpl dbProxyService;

    @Autowired
    @Lazy
    private MongoPersistentServiceImpl mongoProxyService;

    @Autowired
    @Lazy
    private EsPersistentServiceImpl esProxyService;


    @Autowired
    @Lazy
    private IDSSettingService dsSettingService;

    public IPersistentService getProxyService(String datasource)
    {
        return getProxyService(getDSLink(datasource));
    }

    public IPersistentService getProxyService(DSLink link)
    {
        if(link.isMongodb())
            return mongoProxyService;
        else if(link.isElasticSearch())
            return esProxyService;
        else if(link.isCassandra())
            return dbProxyService;
        else
            return dbProxyService;
    }

    public DSLink getDSLink(String datasource) {
        return dsSettingService.getDataSource(datasource);
    }



    @Cacheable( value="syspssystem",key = "'row:sys-dst-sys-local'")
    public Map<String, DstSystemModel> getLocalSystemModels()
    {
        Map<String,DstSystemModel> models=null;
        File modelDir=new File(getModelPath());
        Path systemJSON=Paths.get(modelDir.getAbsolutePath(),"SYSTEM.json");
        try {
            if(!Files.exists(systemJSON))
                models=LoadLocalSystemModels();
            else
                models=JSON.parseObject(new String(Files.readAllBytes(systemJSON), StandardCharsets.UTF_8), new TypeReference<LinkedHashMap<String, DstSystemModel>>(){});;

        } catch (IOException e) {
            throw new RuntimeException("解析文件失败SYSTEM.json",e);
        }
        return models;
    }


    @CacheEvict( value="syspssystem",key = "'row:sys-dst-sys-local'")
    public void resetLocalSystemModels()
    {

    }

    @CacheEvict( value="syspssystem",key = "'row:sys-dst-sys-local'")
    public Map<String,DstSystemModel> LoadLocalSystemModels()
    {
        Map<String,DstSystemModel> models=new LinkedHashMap<>();
        File modelDir=new File(getModelPath());
        if(modelDir.exists())
        {
            for(File sysDir:modelDir.listFiles())
            {
                if(!sysDir.isDirectory())
                    continue;
                File repoDir=new File(sysDir,"repo");
                if(!repoDir.exists())
                    continue;

                for(File entityDir:repoDir.listFiles())
                {
                    Path json=Paths.get(entityDir.getAbsolutePath(),"domain",entityDir.getName()+".json");
                    if(Files.exists(json))
                    {
                        try{
                            PojoSchema pojoSchema=PojoSchema.fromPath(json);
                            String system=pojoSchema.getSystem();
                            if(StringUtils.isEmpty(system))
                                system=sysDir.getName();
                            String systemName=pojoSchema.getOptions().getSystemName();
                            if(StringUtils.isEmpty(systemName))
                                systemName=system;
                            DstSystemModel dstSystemModel=new DstSystemModel().setPssystemid(system).setPssystemname(systemName).setLastModify(json.toFile().lastModified());
                            if((!models.containsKey(system.toLowerCase()))||(dstSystemModel.getLastModify()>models.get(system.toLowerCase()).getLastModify()))
                                models.put(system.toLowerCase(), dstSystemModel);
                        }catch (Exception ex){}
                        break;
                    }
                }
            }
            if(models.size()>0)
            {
                try {
                    Files.write(Paths.get(modelDir.getAbsolutePath(),"SYSTEM.json"), JSON.toJSONBytes(models));
                } catch (IOException e) {
                }
            }
        }
        return models;
    }


    @Cacheable( value="meta-entities", key = "'system:keymap-'+#p0")
    public Map<String,String> getEntitiyIdsBySystem(String system) throws Exception {

        Map<String, String> entities = new HashMap<>();
        DstSystemModel dstSystemModel=dynamicService.findAllDynamicModel().get(system);
        if(dstSystemModel!=null)
        {
            system = dstSystemModel.getPssystemid();
            Map<String, MetaEntityModel> entityModels = dynamicService.getEntities(system);
            entityModels.entrySet().forEach(entry->{
                String id=entry.getValue().getSystemId().concat(".domain.").concat(entry.getValue().getEntityName());
                entities.put(entry.getKey(),id);
            });
        }
        else
        {
            dstSystemModel=proxy.getLocalSystemModels().get(system);
            Assert.notNull(dstSystemModel,"未找到对应的系统模型"+system);
            system = dstSystemModel.getPssystemid();

            File repoDir=Paths.get(getModelPath(),system,"repo").toFile();
            for(File entityDir:repoDir.listFiles())
            {
                Path json=Paths.get(entityDir.getAbsolutePath(),"domain",entityDir.getName()+".json");
                if(Files.exists(json))
                {
                    try{
                        PojoSchema pojoSchema=PojoSchema.fromPath(json);
                        String entityName=pojoSchema.getName();
                        String id=system.concat(".domain.").concat(entityName);

                        entities.put(entityName, id);
                        if(!entityName.equals(entityName.toLowerCase())) {
                            entities.put(entityName.toLowerCase(), id);
                        }
                        String codeName=pojoSchema.getCodeName();
                        if((StringUtils.isEmpty(codeName))&&(!entities.containsKey(codeName))) {
                            entities.put(codeName, id);
                            if(!codeName.equals(codeName.toLowerCase()))
                                entities.put(codeName.toLowerCase(), id);
                            String pluralize= Inflector.getInstance().pluralize(codeName).toLowerCase();
                            if(!entities.containsKey(pluralize)) {
                                entities.put(pluralize, id);
                            }
                        }
                    }catch (Exception ex){}

                }
            }

        }

        return entities;
    }


    public EntityModel getEntityModel(String system,String entity)
    {
        try {
            return dynamicService.getDynamicEntity(system,entity);
        } catch (Exception exception) {
            throw new RuntimeException("getEntityModel未找到实体"+system+"."+entity,exception);
        }
    }

    public DOModel loadDOModel(String system,String entity)
    {
        EntityModel entityModel= proxy.getEntityModel(system,entity);
        Assert.notNull(entityModel,"loadDOModel未找到实体"+system+"."+entity);
        PojoSchema schema = TransUtils.EntityModelModel2Schema(entityModel);
        Assert.notNull(schema,"loadDOModel未找到实体"+system+"."+entity);
        DOModel doModel=new DOModel();
        doModel.setSchema(schema);
        for (String dsType : entityModel.getDsTypes()) {
            POSchema poSchema = TransUtils.EntityModelModel2PO(entityModel, dsType);
            if (poSchema != null) {
                doModel.addPOSchema(dsType, poSchema);
            }
        }
        return doModel;
    }


    @Cacheable( value="domodel",key = "'row:'+#p0+'.domain.'+#p1")
    public DOModel getDOModel(String system,String entity)
    {
        String key=system+".domain."+entity;
        DOModel doModel=null;
        PojoSchema schema=null;
        Path storePath = Paths.get(ModelService.MODEL_PATH,system,"repo",entity,"domain",entity+".json");
        if(!Files.exists(storePath))
        {
            try {
                String entityTag=proxy.getEntitiyIdsBySystem(system).get(entity);
                Assert.hasLength(entityTag,"获取模型失败"+key);
                if(!key.equals(entityTag))
                {
                    String[] args=entityTag.split("[.]");
                    system=args[0];
                    entity=args[2];
                    storePath = Paths.get(ModelService.MODEL_PATH,system,"repo",entity,"domain",entity+".json");
                }
            } catch (Exception exception) {
                throw new RuntimeException("获取模型失败"+key,exception);
            }
        }

        if(!Files.exists(storePath))
        {
            doModel = proxy.loadDOModel(system,entity);
            schema = doModel.getSchema();
            if(schema!=null)
            {
                if(doModel.getPoSchemas()!=null)
                {
                    Path poPath = Paths.get(ModelService.MODEL_PATH,doModel.getSystemId(),"repo",doModel.getName(),"repository",doModel.getName()+".json");
                    try {
                        File dir=poPath.getParent().toFile();
                        if(!dir.exists())
                            dir.mkdirs();
                        Files.write(poPath, JSON.toJSONBytes(doModel.getPoSchemas()));
                    } catch (Exception e) {
                        throw new RuntimeException("保存文件失败POSchemas:"+poPath.toString());
                    }
                }
                schema.writeTo(Paths.get(ModelService.MODEL_PATH,doModel.getSystemId(),"repo",doModel.getName(),"domain",doModel.getName()+".json"));
            }
        }
        else {
            doModel = new DOModel();
            schema = PojoSchema.fromPath(storePath);
            if(schema!=null) {
                doModel.setSchema(schema);
                Path poPath = Paths.get(ModelService.MODEL_PATH, system, "repo", entity, "repository", entity + ".json");
                if (Files.exists(poPath)) {
                    try {
                        doModel.setPoSchemas(JSON.parseObject(new String(Files.readAllBytes(poPath), StandardCharsets.UTF_8), new TypeReference<LinkedHashMap<String, POSchema>>() {
                        }));
                    } catch (IOException e) {

                    }
                }
            }
        }
        Assert.notNull(schema,"未找到对应的模型"+key);
        return doModel;
    }


    @Cacheable( value="pomodel",key = "'row:'+#p0+'._table.'+#p1+'.'+#p2")
    public POSchema getPOSchema(String system,String table,String type)
    {
        String key=system+"._table."+table.toUpperCase()+"."+type.toLowerCase();
        if(type.equalsIgnoreCase("mongodb"))
        {
            DOModel doModel=proxy.getDOModel(system,table);
            if(doModel!=null&&doModel.getSchema()!=null)
            {
                return doModel.getPOSchema(type);
            }
        }
        else
        {
            Path storePath = Paths.get(ModelService.MODEL_PATH,system,"repo","_table",table.toUpperCase(),type.toUpperCase()+".json");
            if(Files.exists(storePath))
                return POSchema.fromPath(storePath);

            String vendorProvider=POSchema.provider.get(type.toLowerCase());
            if(!type.equalsIgnoreCase(vendorProvider))
            {
                storePath = Paths.get(ModelService.MODEL_PATH,system,"repo","_table",table.toUpperCase(),POSchema.provider.get(vendorProvider)+".json");
                if(Files.exists(storePath))
                    return POSchema.fromPath(storePath);
            }
        }

        throw new IllegalArgumentException("未找到对应的模型"+key);
    }


    @PostConstruct
    public void init()
    {
        MODEL_PATH=this.getModelPath();
    }

    @Caching( evict = {
            @CacheEvict( value="domodel",key = "'row:'+#p0" )
    })
    public void resetDOModel(String tag)
    {

    }

    public void resetDOModel(String system,String entity)
    {
        try {
            proxy.getEntitiyIdsBySystem(system).entrySet().forEach(entry->{
                String standardSystem="";
                String standardEntity="";
                String[] args=entry.getValue().split("[.]");
                standardSystem=args[0];
                standardEntity=args[2];
                if(entry.getKey().equals(standardEntity)||entry.getKey().equals(entity)) {
                    proxy.resetDOModel(standardSystem.concat(".domain.").concat(entry.getKey()));
                    if(!standardSystem.equals(standardSystem.toLowerCase()))
                        proxy.resetDOModel(standardSystem.toLowerCase().concat(".domain.").concat(entry.getKey()));

                    if(!system.equals(standardSystem))
                    {
                        proxy.resetDOModel(system.concat(".domain.").concat(entry.getKey()));
                        if(!system.equals(system.toLowerCase()))
                            proxy.resetDOModel(system.toLowerCase().concat(".domain.").concat(entry.getKey()));
                    }
                }
            });
        } catch (Exception exception) {
        }
    }

    @Scheduled(cron="0 */1 * * * ?")
    public void refreshDynamicModel()
    {
        refreshDynamicModel(dynamicService.getAllDynamicModelVersion());
    }

    public void refreshDynamicModel(String system)
    {
        refreshDynamicModel(dynamicService.getDynamicModelVersion(system));
    }

    private synchronized void refreshDynamicModel(Map<String,Long> entities)
    {
        Set<String> changedSystems=new LinkedHashSet<>();
        entities.entrySet().forEach(item->{
            String arr[]=item.getKey().split("[.]");
            String system=arr[0];
            String entity=arr[1];
            Long systemModify=item.getValue();
            File json=Paths.get(this.getModelPath(),system,"repo",entity,"domain",entity+".json").toFile();
            Long doModify=0L;
            if(json.exists())
                doModify=json.lastModified();
            if(doModify<systemModify)
            {
                DOModel doModel = loadDOModel(system,entity);
                if(doModify==0L)
                    log.info("新增模型[".concat(doModel.getName()).concat("]").concat(doModel.getTitle()));
                else {
                    resetDOModel(system,entity);
                    log.info("刷新模型[".concat(doModel.getName()).concat("]").concat(doModel.getTitle()).concat(",").concat(doModify.toString()).concat("->").concat(systemModify.toString()));
                }
                PojoSchema schema = doModel.getSchema();
                if(schema!=null)
                {
                    changedSystems.add(system);
                    if(doModel.getPoSchemas()!=null)
                    {
                        Path poPath = Paths.get(ModelService.MODEL_PATH,doModel.getSystemId(),"repo",doModel.getName(),"repository",doModel.getName()+".json");
                        try {
                            File dir=poPath.getParent().toFile();
                            if(!dir.exists())
                                dir.mkdirs();
                            Files.write(poPath, JSON.toJSONBytes(doModel.getPoSchemas()));
                        } catch (Exception e) {
                            throw new RuntimeException("保存文件失败POSchemas:"+poPath.toString());
                        }
                        doModel.getPoSchemas().values().forEach(poSchema->{
                            mergeTableSchema(doModel.getSystemId(),poSchema,systemModify);
                        });
                    }
                    schema.writeTo(Paths.get(ModelService.MODEL_PATH,doModel.getSystemId(),"repo",doModel.getName(),"domain",doModel.getName()+".json"));
                }
            }

        });
        if(changedSystems.size()>0) {
            LoadLocalSystemModels();
        }
    }

    public void mergeTableSchema(String system, POSchema poSchema, Long timestamp)
    {
        POSchema newSchema= JSONObject.parseObject(JSON.toJSONString(poSchema),POSchema.class);
        File dir=Paths.get(ModelService.MODEL_PATH,system,"repo","_table",poSchema.getName().toUpperCase()).toFile();
        if(!dir.exists())
            dir.mkdirs();
        Path json=Paths.get(ModelService.MODEL_PATH,system,"repo","_table",poSchema.getName().toUpperCase(),poSchema.getDsType().toLowerCase()+".json");
        if(Files.exists(json)&&json.toFile().lastModified()>timestamp)
        {
            POSchema old=POSchema.fromPath(json);
            Set<String> oldex=new HashSet<>();
            Set<String> newex=new HashSet<>();
            if(old.getColumns()!=null)
            {
                old.getColumns().forEach(column -> {
                    POSchema.Column newColumn=newSchema.getColumn(column.getName());
                    if (newColumn==null)
                    {
                        newSchema.addColumn(JSON.parseObject(JSON.toJSONString(column),POSchema.Column.class));
                        oldex.add(column.getName());
                    }
                    else if(!ObjectUtils.isEmpty(column.getSearchModes()))
                    {
                        newColumn.putSearchModes(String.join(",",column.getSearchModes()));
                    }
                });
            }

            if(poSchema.getColumns()!=null)
            {
                poSchema.getColumns().forEach(column -> {
                    if (old.getColumn(column.getName())==null)
                        newex.add(column.getName());
                });
            }

            if(old.getTransients()!=null)
            {
                old.getTransients().forEach(column -> {
                    POSchema.Column newColumn=newSchema.getColumn(column.getName());
                    if (newColumn==null)
                    {
                        newSchema.addTransient(JSON.parseObject(JSON.toJSONString(column),POSchema.Column.class));
                        oldex.add(column.getName());
                    }
                    else if(!ObjectUtils.isEmpty(column.getSearchModes()))
                    {
                        newColumn.putSearchModes(String.join(",",column.getSearchModes()));
                    }
                });
            }

            if(poSchema.getTransients()!=null)
            {
                poSchema.getTransients().forEach(column -> {
                    if (old.getColumn(column.getName())==null)
                        newex.add(column.getName());
                });
            }

            if(!ObjectUtils.isEmpty(old.getForeignKeyConstraints()))
            {
                old.getForeignKeyConstraints().forEach(fk->{
                    if(!newSchema.getForeignKey().contains(fk.getConstraintName()))
                        newSchema.addForeignKeyConstraint(JSON.parseObject(JSON.toJSONString(fk),POSchema.ForeignKeyConstraint.class));
                });
            }
            if(!ObjectUtils.isEmpty(old.getSegments()))
            {
                old.getSegments().forEach(script -> {
                    String key= DataObject.getStringValue(script.getName(),"").concat(".").concat(DataObject.getStringValue(script.getVendorProvider(),""));
                    if(newSchema.getSegment(key)==null)
                    {
                        newSchema.addSegment(JSON.parseObject(JSON.toJSONString(script),POSchema.Segment.class));
                    }
                });
            }

            if(oldex.size()>0&&newex.size()==0)
                newSchema.setDefaultQueryScript(JSON.parseObject(JSON.toJSONString(old.getDefaultQueryScript()),POSchema.Segment.class));
            else if(oldex.size()>0&&newex.size()>0)
            {
                newSchema.refreshColumnMaps();
                String sql="select ";
                String cols="";
                if(newSchema.getBaseColumnMap()!=null)
                {
                    cols=String.join(",",newSchema.getBaseColumnMap().keySet());
                }
                else
                    cols="t1.*";
                sql += (cols+" from "+newSchema.getName()+" t1 ");

                if(newSchema.isLogicValid())
                {
                    sql += " where t1."+newSchema.getLogicValidCond();
                }
                newSchema.setDefaultQueryScriptSQL(sql);
            }

        }
        POSchema.writeTo(newSchema,json);
    }

}
