package cn.ibizlab.core.data.model;

import cn.ibizlab.core.data.dto.BaseData;
import cn.ibizlab.util.errors.BadRequestAlertException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.*;

@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonIgnoreProperties(ignoreUnknown = true)
public class POSchema {

    @JSONField(ordinal = 1)
    private String name;

    @JSONField(ordinal = 2)
    private String remarks;

    @JSONField(ordinal = 3)
    private String dsType;

    @JSONField(ordinal = 4)
    private String defaultDataSource;

    @JSONField(ordinal = 5)
    private String logicVal;
    @JSONField(ordinal = 6)
    private String logicDelVal;

    public String getLogicVal() {
        if(StringUtils.isEmpty(logicVal))
            return "1";
        return logicVal;
    }

    public String getLogicDelVal() {
        if(StringUtils.isEmpty(logicDelVal))
            return "0";
        return logicDelVal;
    }

    @JSONField(ordinal = 7)
    private List<Column> columns;

    @JSONField(ordinal = 8)
    private List<Column> transients;

    public POSchema setColumns(List<Column> columns)
    {
        this.columns=columns;
        refreshColumnMaps();
        return this;
    }

    public POSchema setTransients(List<Column> transients)
    {
        this.transients=transients;
        refreshColumnMaps();
        return this;
    }

    public POSchema addColumn(Column column)
    {
        if(columns==null)
            columns=new ArrayList<>();
        columns.add(column);
        refreshColumn(column);
        return this;
    }

    public POSchema addTransient(Column column)
    {
        if(transients==null)
            transients=new ArrayList<>();
        transients.add(column);
        refreshColumn(column);
        return this;
    }

    @JSONField(ordinal = 9)
    private List<ForeignKeyConstraint> foreignKeyConstraints;

    public POSchema addForeignKeyConstraint(ForeignKeyConstraint foreignKeyConstraint)
    {
        if(foreignKeyConstraints==null)
            foreignKeyConstraints=new ArrayList<>();
        foreignKeyConstraints.add(foreignKeyConstraint);

        return this;
    }

    @JsonIgnore
    @JSONField(serialize = false)
    private Map<String,Column> columnMaps;


    @JsonIgnore
    @JSONField(serialize = false)
    private Map<String,String> baseColumnMap;


    @JsonIgnore
    @JSONField(serialize = false)
    private Map<String,String> resultMap;

    @JsonIgnore
    @JSONField(serialize = false)
    private Map<String,String> keyMap;

    @JsonIgnore
    @JSONField(serialize = false)
    private Map<String,Column> searchMap;

    @JsonIgnore
    @JSONField(serialize = false)
    private Map<String,Column> quickSearch;

    @JsonIgnore
    @JSONField(serialize = false)
    public Map<String, Column> getColumnMaps() {
        if(columns!=null&&columnMaps==null)
        {
            refreshColumnMaps();
        }
        return columnMaps;
    }

    @JSONField(ordinal = 10)
    public boolean needTrans = false;

    public void refreshColumnMaps()
    {
        columnMaps=new LinkedHashMap<>();
        resultMap=new LinkedHashMap<>();
        baseColumnMap=new LinkedHashMap<>();
        keyMap=new LinkedHashMap<>();
        searchMap=new LinkedHashMap<>();
        quickSearch=new LinkedHashMap<>();
        columns.forEach(column -> {
            refreshColumn(column);
        });
        if(logicValid&&logicValidColumn==null)
            logicValid=false;
    }

    public void refreshColumn(Column column)
    {
        if(columnMaps==null)
            columnMaps=new LinkedHashMap<>();
        if(resultMap==null)
            resultMap=new LinkedHashMap<>();
        if(baseColumnMap==null)
            baseColumnMap=new LinkedHashMap<>();
        if(keyMap==null)
            keyMap=new LinkedHashMap<>();
        if(searchMap==null)
            searchMap=new LinkedHashMap<>();
        if(quickSearch==null)
            quickSearch=new LinkedHashMap<>();

        columnMaps.put(column.getName().toLowerCase(),column);

        if(!ObjectUtils.isEmpty(column.getSearchModes()))
        {
            column.getSearchModes().forEach(mode->{
                if(!mode.equalsIgnoreCase("query"))
                    searchMap.put("n_"+column.getName().toLowerCase()+"_"+mode,column);
                else
                    quickSearch.put(column.getName().toLowerCase(),column);
            });
        }

        if(!column.isComputed())
        {
            if(column.isLogicValid()) {
                this.setLogicValid(true);
                this.setLogicValidColumn(column);
                if(StringUtils.isEmpty(column.getDefaultValue()))
                    column.setDefaultValue(this.getLogicVal());

                logicValidCond  =  " "+column.getName()+"=";
                logicValidDelCond = " "+column.getName()+"=";
                if(column.isText()) {
                    logicValidCond += ("'" + this.getLogicVal() + "' ");
                    logicValidDelCond += ("'" + this.getLogicDelVal() + "' ");
                }
                else {
                    logicValidCond += (this.getLogicVal() + " ");
                    logicValidDelCond += (this.getLogicDelVal() + " ");
                }
            }
            if(column.isTenant())
                this.setTenantColumn(column);
            if(column.isCreateTime())
                this.setCreateTimeColumn(column);
            if(column.isLastModify())
                this.setLastModifyColumn(column);
        }
        if((!StringUtils.isEmpty(column.getAlias()))&&(!column.getAlias().equalsIgnoreCase(column.getName())))
        {
            needTrans=true;
            columnMaps.put(column.getAlias().toLowerCase(), column);
            resultMap.put(column.getName().toLowerCase(),column.getAlias().toLowerCase());
            if(!column.isComputed())
            {
                baseColumnMap.put(column.getName().toLowerCase(),column.getAlias().toLowerCase());
                if(column.isPrimaryKey())
                    keyMap.put(column.getName().toLowerCase(),column.getAlias().toLowerCase());
            }

            if(!ObjectUtils.isEmpty(column.getSearchModes()))
            {
                column.getSearchModes().forEach(mode->{
                    if(!mode.equalsIgnoreCase("query"))
                        searchMap.put("n_"+column.getAlias().toLowerCase()+"_"+mode,column);
                });
            }
        }
        else
        {
            resultMap.put(column.getName().toLowerCase(), column.getName().toLowerCase());
            if(!column.isComputed())
            {
                baseColumnMap.put(column.getName().toLowerCase(), column.getName().toLowerCase());
                if(column.isPrimaryKey())
                    keyMap.put(column.getName().toLowerCase(),column.getName().toLowerCase());
            }
        }

    }

    @JsonIgnore
    @JSONField(serialize = false)
    public Map<String, String> getResultMap() {
        if(columns!=null&&resultMap==null)
        {
            refreshColumnMaps();
        }
        return resultMap;
    }

    @JsonIgnore
    @JSONField(serialize = false)
    public Map<String, String> getBaseColumnMap() {
        if(columns!=null&&baseColumnMap==null)
        {
            refreshColumnMaps();
        }
        return baseColumnMap;
    }



    @JsonIgnore
    @JSONField(serialize = false)
    public Map<String, String> getKeyMap() {
        if(columns!=null&&keyMap==null)
        {
            refreshColumnMaps();
        }
        return keyMap;
    }

    @JsonIgnore
    @JSONField(serialize = false)
    public Map<String, Column> getSearchMap() {
        if(columns!=null&&searchMap==null)
        {
            refreshColumnMaps();
        }
        return searchMap;
    }

    @JsonIgnore
    @JSONField(serialize = false)
    public Map<String, Column> getQuickSearch() {
        if(columns!=null&&quickSearch==null)
        {
            refreshColumnMaps();
        }
        return quickSearch;
    }

    public Column getColumn(String name)
    {
        if(getColumnMaps()!=null)
        {
            return columnMaps.get(name.toLowerCase());
        }
        return null;
    }

    @JsonIgnore
    @JSONField(serialize = false)
    private Column lastModifyColumn;


    @JsonIgnore
    @JSONField(serialize = false)
    private Column createTimeColumn;


    @JsonIgnore
    @JSONField(serialize = false)
    private Column tenantColumn;


    @JsonIgnore
    @JSONField(serialize = false)
    private boolean logicValid=false;
    @JsonIgnore
    @JSONField(serialize = false)
    public boolean isLogicValid()
    {
        if(logicValid&&logicValidColumn==null) {
            if (columns != null) {
                for (Column col:columns) {
                    if (col.isLogicValid()) {
                        logicValidColumn = col;
                        return logicValid;
                    }
                }
            }
            logicValid = false;
        }
        return logicValid;
    }

    @JsonIgnore
    @JSONField(serialize = false)
    private Column logicValidColumn;


    @Getter
    @Setter
    @NoArgsConstructor
    @Accessors(chain = true)
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class Column{
        @JSONField(ordinal = 1)
        private String name;
        @JSONField(ordinal = 2)
        private String remarks;
        @JSONField(ordinal = 3)
        private String type;
        @JSONField(ordinal = 4)
        private Integer length;
        @JSONField(ordinal = 5)
        private Integer precision;
        @JSONField(ordinal = 6)
        private String defaultValue;
        @JSONField(ordinal = 7)
        private Boolean autoIncrement;
        @JSONField(ordinal = 8)
        private Boolean computed;
        @JSONField(ordinal = 9)
        private String alias;
        @JSONField(ordinal = 10)
        private String predefined;
        @JSONField(ordinal = 11)
        private Set<String> searchModes;

        public Column putSearchModes(String searchModes)
        {

            if(!StringUtils.isEmpty(searchModes))
            {
                if(this.searchModes==null)
                    this.searchModes=new LinkedHashSet<>();
                for(String mode:searchModes.split(","))
                    this.searchModes.add(mode);
            }
            return this;
        }

        public Column setAlias(String alias)
        {
            if((!StringUtils.isEmpty(name))&&(!name.equalsIgnoreCase(alias)))
                this.alias=alias;
            return this;
        }

        @JsonIgnore
        @JSONField(serialize = false)
        public boolean isComputed()
        {
            return this.getComputed()!=null&&this.getComputed();
        }

        @JsonIgnore
        @JSONField(serialize = false)
        public boolean isAutoIncrement()
        {
            return this.getAutoIncrement()!=null&&this.getAutoIncrement();
        }

        @JsonIgnore
        @JSONField(serialize = false)
        public boolean isNullable()
        {
            return this.getConstraints()!=null&&this.getConstraints().isNullable();
        }

        @JsonIgnore
        @JSONField(serialize = false)
        public boolean isPrimaryKey()
        {
            return this.getConstraints()!=null&&this.getConstraints().isPrimaryKey();
        }

        @JsonIgnore
        @JSONField(serialize = false)
        public boolean isLogicValid()
        {
            return "LOGICVALID".equals(this.getPredefined());
        }

        @JsonIgnore
        @JSONField(serialize = false)
        public boolean isLastModify()
        {
            return "UPDATEDATE".equals(this.getPredefined());
        }

        @JsonIgnore
        @JSONField(serialize = false)
        public boolean isCreateTime()
        {
            return "CREATEDATE".equals(this.getPredefined());
        }

        @JsonIgnore
        @JSONField(serialize = false)
        public boolean isTenant()
        {
            return "TENANT".equals(this.getPredefined());
        }

        @JsonIgnore
        @JSONField(serialize = false)
        public boolean isText()
        {
            String type=this.getType().toUpperCase();
            return type.indexOf("TEXT")>=0||type.indexOf("CHAR")>=0||type.indexOf("LOB")>=0;
        }
        @JsonIgnore
        @JSONField(serialize = false)
        public boolean isNumber()
        {
            String type=this.getType().toUpperCase();
            return type.indexOf("NUM")>=0||type.indexOf("FLOAT")>=0||type.indexOf("DOUBLE")>=0||type.indexOf("DECIMAL")>=0;
        }
        @JsonIgnore
        @JSONField(serialize = false)
        public boolean isInt()
        {
            String type=this.getType().toUpperCase();
            return type.indexOf("INT")==0;
        }

        @JsonIgnore
        @JSONField(serialize = false)
        public boolean isDateTime()
        {
            String type=this.getType().toUpperCase();
            return type.indexOf("DATE")>=0||type.indexOf("TIME")>=0;
        }

        @JSONField(ordinal = 12)
        private Constraints constraints;

        public Constraints getConstraints(boolean createWhenNotExist)
        {
            if(createWhenNotExist)
            {
                if(constraints==null)
                    constraints=new Constraints();
            }
            return constraints;
        }

    }

    @Getter
    @Setter
    @NoArgsConstructor
    @Accessors(chain = true)
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class Constraints{
        private Boolean nullable;
        private Boolean primaryKey;
        private String primaryKeyName;
        private String foreignKeyName;
        private String referencedTableName;
        private String referencedColumnNames;

        @JsonIgnore
        @JSONField(serialize = false)
        public boolean isNullable()
        {
            return this.getNullable()!=null&&this.getNullable();
        }

        @JsonIgnore
        @JSONField(serialize = false)
        public boolean isPrimaryKey()
        {
            return this.getPrimaryKey()!=null&&this.getPrimaryKey();
        }

    }

    @Getter
    @Setter
    @NoArgsConstructor
    @Accessors(chain = true)
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class ForeignKeyConstraint{
        private String baseColumnNames;
        private String baseTableName;
        private String constraintName;
        private String referencedTableName;
        private String referencedColumnNames;
        private Boolean validate;
        private String onUpdate;
        private String onDelete;

    }

    @Getter
    @Setter
    @NoArgsConstructor
    @Accessors(chain = true)
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class Segment{
        private String name;
        private String vendorProvider;
        private String declare;
        private String body;
        private String format;
        private Map params;

        public Segment setVendorProvider(String vendorProvider)
        {
            if(!StringUtils.isEmpty(vendorProvider))
                this.vendorProvider=provider.get(vendorProvider.toLowerCase());
            return this;
        }

    }


    @JSONField(ordinal = 11)
    public List<Segment> segments;

    public POSchema addSegment(Segment segment)
    {
        if(segments==null)
            segments=new ArrayList<>();
        segments.add(segment);
        return this;
    }

    public static Map<String, String> provider = new HashMap<String, String>(){{
        put("oracle", "oracle");
        put("mysql", "mysql");
        put("mysql5", "mysql");
        put("postgresql", "postgresql");
        put("mppdb", "postgresql");
        put("dm", "oracle");
        put("dameng", "oracle");
        put("gbase", "oracle");
        put("h2", "mysql");
    }};



    public Segment getSegment(String name,String type)
    {
        if(segments!=null)
        {
            if(StringUtils.isEmpty(type))
                type=this.getDsType();
            String vendorProvider=StringUtils.isEmpty(type)?"":provider.get(type.toLowerCase());
            for(Segment script:segments)
            {
                if(script.getName().toLowerCase().indexOf(name.toLowerCase())>=0&&(StringUtils.isEmpty(vendorProvider)||script.getVendorProvider().indexOf(vendorProvider)>=0||StringUtils.isEmpty(script.getVendorProvider())))
                    return script;
            }
        }
        else
		{
            segments=new ArrayList<>();
		}
        return null;
    }

    @JSONField(ordinal = 12)
    public Segment defaultQueryScript;

    public POSchema setDefaultQueryScript(String sql)
    {
        defaultQueryScript=new Segment().setName("default_query_script").setVendorProvider("").setBody(sql);
        return this;
    }

    public Segment getDefaultQueryScript()
    {
        if(defaultQueryScript==null)
        {
            Segment segment=this.getSegment("dq-view-",this.getDsType());
            if(segment==null)
                segment=this.getSegment("dq-default-",this.getDsType());
            if(segment!=null)
            {
                setDefaultQueryScript(segment.getBody());
                return defaultQueryScript;
            }
            else if(!StringUtils.isEmpty(this.getDsType()))
            {
                segment=this.getSegment("dq-view-","");
                if(segment==null)
                    segment=this.getSegment("dq-default-","");
                if(segment!=null)
                {
                    setDefaultQueryScript(segment.getBody().replace("`","").replace("[","").replace("]",""));
                    return defaultQueryScript;
                }
            }

            defaultQueryScript=new Segment();
            defaultQueryScript.setName("default_query_script");
            defaultQueryScript.setVendorProvider("");
            String sql="select ";

            String cols="";
            if(getBaseColumnMap()!=null)
            {
                cols=String.join(",",baseColumnMap.keySet());
            }
            else
                cols="t1.*";

            sql += (cols+" from "+name+" t1 ");

            if(isLogicValid())
            {

                sql += " where t1."+this.getLogicValidCond();

            }
            defaultQueryScript.setBody(sql);
        }
        return defaultQueryScript;
    }

    @JsonIgnore
    @JSONField(serialize = false)
    public String logicValidCond;
    @JsonIgnore
    @JSONField(serialize = false)
    public String logicValidDelCond;





    @JSONField(serialize = false)
    @JsonIgnore
    private boolean built = false;

    @JSONField(serialize = false)
    @JsonIgnore
    public synchronized POSchema build()
    {
        if(!built)
        {
            refreshColumnMaps();
            built=true;
        }
        return this;
    }

    public BaseData trans(BaseData source)
    {
        if(!needTrans)
            return source;
        else {
            BaseData target = new BaseData();
            for (Map.Entry<String, String> entry : resultMap.entrySet()) {
                if(source.keySet().contains(entry.getKey()))
                    target.set(entry.getValue(),source.get(entry.getKey()));
            }
            return target;
        }
    }

    public List<BaseData> trans(List<BaseData> source)
    {
        if(!needTrans)
            return source;
        else {
            List<BaseData> target = new ArrayList<>();
            source.forEach(item->target.add(trans(item)));
            return target;
        }
    }


    public POSchema writeTo(Path path)
    {
        return writeTo(this,path);
    }

    public static POSchema fromPath(Path path)
    {
        try {
            if(!Files.exists(path))
                throw new BadRequestAlertException("读取文件失败","POSchema",path.toString());
            return JSON.parseObject(Files.readAllBytes(path),POSchema.class);

        } catch (Exception e) {
            throw new BadRequestAlertException("解析文件失败","POSchema",path.toString());
        }
    }

    public static POSchema writeTo(POSchema poSchema,Path path)
    {
        try {
            File dir=path.getParent().toFile();
            if(!dir.exists())
                dir.mkdirs();
            Files.write(path, JSON.toJSONBytes(poSchema), StandardOpenOption.CREATE);
        } catch (Exception e) {
            throw new BadRequestAlertException("保存文件失败","POSchema",path.toString());
        }
        return poSchema;
    }


}
