// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package org.apache.doris.backup;

import org.apache.doris.backup.RestoreFileMapping.IdChain;
import org.apache.doris.catalog.MaterializedIndex;
import org.apache.doris.catalog.OlapTable;
import org.apache.doris.catalog.Partition;
import org.apache.doris.catalog.Table;
import org.apache.doris.catalog.Tablet;
import org.apache.doris.common.FeConstants;
import org.apache.doris.common.io.Text;
import org.apache.doris.common.io.Writable;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/*
 * This is a memory structure mapping the job info file in repository.
 * It contains all content of a job info file.
 * It also be used to save the info of a restore job, such as alias of table and meta info file path
 */
public class BackupJobInfo implements Writable {
    private static final Logger LOG = LogManager.getLogger(BackupJobInfo.class);

    public String name;
    public String dbName;
    public long dbId;
    public long backupTime;
    public Map<String, BackupTableInfo> tables = Maps.newHashMap();
    public boolean success;

    public int metaVersion;

    // This map is used to save the table alias mapping info when processing a restore job.
    // origin -> alias
    public Map<String, String> tblAlias = Maps.newHashMap();

    public boolean containsTbl(String tblName) {
        return tables.containsKey(tblName);
    }

    public BackupTableInfo getTableInfo(String tblName) {
        return tables.get(tblName);
    }

    public void retainTables(Set<String> tblNames) {
        Iterator<Map.Entry<String, BackupTableInfo>> iter = tables.entrySet().iterator();
        while (iter.hasNext()) {
            if (!tblNames.contains(iter.next().getKey())) {
                iter.remove();
            }
        }
    }

    public void setAlias(String orig, String alias) {
        tblAlias.put(orig, alias);
    }

    public String getAliasByOriginNameIfSet(String orig) {
        return tblAlias.containsKey(orig) ? tblAlias.get(orig) : orig;
    }

    public String getOrginNameByAlias(String alias) {
        for (Map.Entry<String, String> entry : tblAlias.entrySet()) {
            if (entry.getValue().equals(alias)) {
                return entry.getKey();
            }
        }
        return alias;
    }

    public static class BackupTableInfo {
        public String name;
        public long id;
        public Map<String, BackupPartitionInfo> partitions = Maps.newHashMap();

        public boolean containsPart(String partName) {
            return partitions.containsKey(partName);
        }

        public BackupPartitionInfo getPartInfo(String partName) {
            return partitions.get(partName);
        }

        public void retainPartitions(Collection<String> partNames) {
            if (partNames == null || partNames.isEmpty()) {
                // retain all
                return;
            }
            Iterator<Map.Entry<String, BackupPartitionInfo>> iter = partitions.entrySet().iterator();
            while (iter.hasNext()) {
                if (!partNames.contains(iter.next().getKey())) {
                    iter.remove();
                }
            }
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("name: ").append(name).append(", id: ").append(id);
            sb.append(", partitions: [").append(Joiner.on(", ").join(partitions.keySet())).append("]");
            return sb.toString();
        }
    }

    public static class BackupPartitionInfo {
        public String name;
        public long id;
        public long version;
        public long versionHash;
        public Map<String, BackupIndexInfo> indexes = Maps.newHashMap();

        public BackupIndexInfo getIdx(String idxName) {
            return indexes.get(idxName);
        }
    }

    public static class BackupIndexInfo {
        public String name;
        public long id;
        public int schemaHash;
        public List<BackupTabletInfo> tablets = Lists.newArrayList();

        public BackupTabletInfo getTablet(long tabletId) {
            for (BackupTabletInfo backupTabletInfo : tablets) {
                if (backupTabletInfo.id == tabletId) {
                    return backupTabletInfo;
                }
            }
            return null;
        }
    }

    public static class BackupTabletInfo {
        public long id;
        public List<String> files = Lists.newArrayList();
    }

    // eg: __db_10001/__tbl_10002/__part_10003/__idx_10002/__10004
    public String getFilePath(String db, String tbl, String part, String idx, long tabletId) {
        if (!db.equalsIgnoreCase(dbName)) {
            LOG.debug("db name does not equal: {}-{}", dbName, db);
            return null;
        }

        BackupTableInfo tblInfo = tables.get(tbl);
        if (tblInfo == null) {
            LOG.debug("tbl {} does not exist", tbl);
            return null;
        }

        BackupPartitionInfo partInfo = tblInfo.getPartInfo(part);
        if (partInfo == null) {
            LOG.debug("part {} does not exist", part);
            return null;
        }

        BackupIndexInfo idxInfo = partInfo.getIdx(idx);
        if (idxInfo == null) {
            LOG.debug("idx {} does not exist", idx);
            return null;
        }

        List<String> pathSeg = Lists.newArrayList();
        pathSeg.add(Repository.PREFIX_DB + dbId);
        pathSeg.add(Repository.PREFIX_TBL + tblInfo.id);
        pathSeg.add(Repository.PREFIX_PART + partInfo.id);
        pathSeg.add(Repository.PREFIX_IDX + idxInfo.id);
        pathSeg.add(Repository.PREFIX_COMMON + tabletId);

        return Joiner.on("/").join(pathSeg);
    }

    // eg: __db_10001/__tbl_10002/__part_10003/__idx_10002/__10004
    public String getFilePath(IdChain ids) {
        List<String> pathSeg = Lists.newArrayList();
        pathSeg.add(Repository.PREFIX_DB + dbId);
        pathSeg.add(Repository.PREFIX_TBL + ids.getTblId());
        pathSeg.add(Repository.PREFIX_PART + ids.getPartId());
        pathSeg.add(Repository.PREFIX_IDX + ids.getIdxId());
        pathSeg.add(Repository.PREFIX_COMMON + ids.getTabletId());

        return Joiner.on("/").join(pathSeg);
    }

    public static BackupJobInfo fromCatalog(long backupTime, String label, String dbName, long dbId,
            Collection<Table> tbls, Map<Long, SnapshotInfo> snapshotInfos) {

        BackupJobInfo jobInfo = new BackupJobInfo();
        jobInfo.backupTime = backupTime;
        jobInfo.name = label;
        jobInfo.dbName = dbName;
        jobInfo.dbId = dbId;
        jobInfo.success = true;
        jobInfo.metaVersion = FeConstants.meta_version;

        // tbls
        for (Table tbl : tbls) {
            OlapTable olapTbl = (OlapTable) tbl;
            BackupTableInfo tableInfo = new BackupTableInfo();
            tableInfo.id = tbl.getId();
            tableInfo.name = tbl.getName();
            jobInfo.tables.put(tableInfo.name, tableInfo);
            // partitions
            for (Partition partition : olapTbl.getPartitions()) {
                BackupPartitionInfo partitionInfo = new BackupPartitionInfo();
                partitionInfo.id = partition.getId();
                partitionInfo.name = partition.getName();
                partitionInfo.version = partition.getVisibleVersion();
                partitionInfo.versionHash = partition.getVisibleVersionHash();
                tableInfo.partitions.put(partitionInfo.name, partitionInfo);
                // indexes
                for (MaterializedIndex index : partition.getMaterializedIndices()) {
                    BackupIndexInfo idxInfo = new BackupIndexInfo();
                    idxInfo.id = index.getId();
                    idxInfo.name = olapTbl.getIndexNameById(index.getId());
                    idxInfo.schemaHash = olapTbl.getSchemaHashByIndexId(index.getId());
                    partitionInfo.indexes.put(idxInfo.name, idxInfo);
                    // tablets
                    for (Tablet tablet : index.getTablets()) {
                        BackupTabletInfo tabletInfo = new BackupTabletInfo();
                        tabletInfo.id = tablet.getId();
                        tabletInfo.files.addAll(snapshotInfos.get(tablet.getId()).getFiles());
                        idxInfo.tablets.add(tabletInfo);
                    }
                }
            }
        }

        return jobInfo;
    }

    public static BackupJobInfo fromFile(String path) throws IOException {
        byte[] bytes = Files.readAllBytes(Paths.get(path));
        String json = new String(bytes, StandardCharsets.UTF_8);
        BackupJobInfo jobInfo = new BackupJobInfo();
        genFromJson(json, jobInfo);
        return jobInfo;
    }

    private static void genFromJson(String json, BackupJobInfo jobInfo) {
        /* parse the json string: 
         * {
         *   "backup_time": 1522231864000,
         *   "name": "snapshot1",
         *   "database": "db1"
         *   "id": 10000
         *   "backup_result": "succeed",
         *   "meta_version" : 40 // this is optional
         *   "backup_objects": {
         *       "table1": {
         *           "partitions": {
         *               "partition2": {
         *                   "indexes": {
         *                       "rollup1": {
         *                           "id": 10009,
         *                           "schema_hash": 3473401,
         *                           "tablets": {
         *                               "10008": ["__10029_seg1.dat", "__10029_seg2.dat"],
         *                               "10007": ["__10029_seg1.dat", "__10029_seg2.dat"]
         *                           },
         *                           "tablets_order": ["10029", "10030"]
         *                       },
         *                       "table1": {
         *                           "id": 10008,
         *                           "schema_hash": 9845021,
         *                           "tablets": {
         *                               "10004": ["__10027_seg1.dat", "__10027_seg2.dat"],
         *                               "10005": ["__10028_seg1.dat", "__10028_seg2.dat"]
         *                           },
         *                           "tablets_order": ["10027, "10028"]
         *                       }
         *                   },
         *                   "id": 10007
         *                   "version": 10
         *                   "version_hash": 1273047329538
         *               },
         *           },
         *           "id": 10001
         *       }
         *   }
         * }
         */
        JSONObject root = new JSONObject(json);
        jobInfo.name = (String) root.get("name");
        jobInfo.dbName = (String) root.get("database");
        jobInfo.dbId = root.getLong("id");
        jobInfo.backupTime = root.getLong("backup_time");
        
        try {
            jobInfo.metaVersion = root.getInt("meta_version");
        } catch (JSONException e) {
            // meta_version does not exist
            jobInfo.metaVersion = FeConstants.meta_version;
        }
        
        JSONObject backupObjs = root.getJSONObject("backup_objects");
        String[] tblNames = JSONObject.getNames(backupObjs);
        for (String tblName : tblNames) {
            BackupTableInfo tblInfo = new BackupTableInfo();
            tblInfo.name = tblName;
            JSONObject tbl = backupObjs.getJSONObject(tblName);
            tblInfo.id = tbl.getLong("id");
            JSONObject parts = tbl.getJSONObject("partitions");
            String[] partsNames = JSONObject.getNames(parts);
            for (String partName : partsNames) {
                BackupPartitionInfo partInfo = new BackupPartitionInfo();
                partInfo.name = partName;
                JSONObject part = parts.getJSONObject(partName);
                partInfo.id = part.getLong("id");
                partInfo.version = part.getLong("version");
                partInfo.versionHash = part.getLong("version_hash");
                JSONObject indexes = part.getJSONObject("indexes");
                String[] indexNames = JSONObject.getNames(indexes);
                for (String idxName : indexNames) {
                    BackupIndexInfo indexInfo = new BackupIndexInfo();
                    indexInfo.name = idxName;
                    JSONObject idx = indexes.getJSONObject(idxName);
                    indexInfo.id = idx.getLong("id");
                    indexInfo.schemaHash = idx.getInt("schema_hash");
                    JSONObject tablets = idx.getJSONObject("tablets");
                    String[] tabletIds = JSONObject.getNames(tablets);

                    JSONArray tabletsOrder = null;
                    if (idx.has("tablets_order")) {
                        tabletsOrder = idx.getJSONArray("tablets_order");
                    }
                    String[] orderedTabletIds = sortTabletIds(tabletIds, tabletsOrder);
                    Preconditions.checkState(tabletIds.length == orderedTabletIds.length);

                    for (String tabletId : orderedTabletIds) {
                        BackupTabletInfo tabletInfo = new BackupTabletInfo();
                        tabletInfo.id = Long.valueOf(tabletId);
                        JSONArray files = tablets.getJSONArray(tabletId);
                        for (Object object : files) {
                            tabletInfo.files.add((String) object);
                        }
                        indexInfo.tablets.add(tabletInfo);
                    }
                    partInfo.indexes.put(indexInfo.name, indexInfo);
                }
                tblInfo.partitions.put(partName, partInfo);
            }
            jobInfo.tables.put(tblName, tblInfo);
        }
        
        String result = root.getString("backup_result");
        if (result.equals("succeed")) {
            jobInfo.success = true;
        } else {
            jobInfo.success = false;
        }
    }

    private static String[] sortTabletIds(String[] tabletIds, JSONArray tabletsOrder) {
        if (tabletsOrder == null || tabletsOrder.toList().isEmpty()) {
            // in previous version, we are not saving tablets order(which was a BUG),
            // so we have to sort the tabletIds to restore the original order of tablets.
            List<String> tmpList = Lists.newArrayList(tabletIds);
            tmpList.sort((o1, o2) -> Long.valueOf(o1).compareTo(Long.valueOf(o2)));
            return tmpList.toArray(new String[0]);
        } else {
            return (String[]) tabletsOrder.toList().toArray(new String[0]);
        }
    }

    public void writeToFile(File jobInfoFile) throws FileNotFoundException {
        PrintWriter printWriter = new PrintWriter(jobInfoFile);
        try {
            printWriter.print(toJson(true).toString());
            printWriter.flush();
        } finally {
            printWriter.close();
        }
    }

    // Only return basic info, table and partitions
    public String getBrief() {
        return toJson(false).toString(1);
    }

    public JSONObject toJson(boolean verbose) {
        JSONObject root = new JSONObject();
        root.put("name", name);
        root.put("database", dbName);
        if (verbose) {
            root.put("id", dbId);
        }
        root.put("backup_time", backupTime);
        JSONObject backupObj = new JSONObject();
        root.put("backup_objects", backupObj);
        root.put("meta_version", FeConstants.meta_version);
        
        for (BackupTableInfo tblInfo : tables.values()) {
            JSONObject tbl = new JSONObject();
            if (verbose) {
                tbl.put("id", tblInfo.id);
            }
            JSONObject parts = new JSONObject();
            tbl.put("partitions", parts);
            for (BackupPartitionInfo partInfo : tblInfo.partitions.values()) {
                JSONObject part = new JSONObject();
                if (verbose) {
                    part.put("id", partInfo.id);
                    part.put("version", partInfo.version);
                    part.put("version_hash", partInfo.versionHash);
                    JSONObject indexes = new JSONObject();
                    part.put("indexes", indexes);
                    for (BackupIndexInfo idxInfo : partInfo.indexes.values()) {
                        JSONObject idx = new JSONObject();
                        idx.put("id", idxInfo.id);
                        idx.put("schema_hash", idxInfo.schemaHash);
                        JSONObject tablets = new JSONObject();
                        JSONArray tabletsOrder = new JSONArray();
                        idx.put("tablets", tablets);
                        for (BackupTabletInfo tabletInfo : idxInfo.tablets) {
                            JSONArray files = new JSONArray();
                            tablets.put(String.valueOf(tabletInfo.id), files);
                            for (String fileName : tabletInfo.files) {
                                files.put(fileName);
                            }
                            // to save the order of tablets
                            tabletsOrder.put(String.valueOf(tabletInfo.id));
                        }
                        indexes.put(idxInfo.name, idx);
                    }
                }
                parts.put(partInfo.name, part);
            }
            backupObj.put(tblInfo.name, tbl);
        }

        root.put("backup_result", "succeed");
        return root;
    }

    public String toString(int indentFactor) {
        return toJson(true).toString(indentFactor);
    }

    public String getInfo() {
        List<String> objs = Lists.newArrayList();
        for (BackupTableInfo tblInfo : tables.values()) {
            StringBuilder sb = new StringBuilder();
            sb.append(tblInfo.name);
            List<String> partNames = tblInfo.partitions.values().stream()
                    .filter(n -> !n.name.equals(tblInfo.name)).map(n -> n.name).collect(Collectors.toList());
            if (!partNames.isEmpty()) {
                sb.append(" PARTITIONS [").append(Joiner.on(", ").join(partNames)).append("]");
            }
            objs.add(sb.toString());
        }
        return Joiner.on(", ").join(objs);
    }

    public static BackupJobInfo read(DataInput in) throws IOException {
        BackupJobInfo jobInfo = new BackupJobInfo();
        jobInfo.readFields(in);
        return jobInfo;
    }

    @Override
    public void write(DataOutput out) throws IOException {
        Text.writeString(out, toJson(true).toString());
        out.writeInt(tblAlias.size());
        for (Map.Entry<String, String> entry : tblAlias.entrySet()) {
            Text.writeString(out, entry.getKey());
            Text.writeString(out, entry.getValue());
        }
    }

    @Override
    public void readFields(DataInput in) throws IOException {
        String json = Text.readString(in);
        genFromJson(json, this);
        int size = in.readInt();
        for (int i = 0; i < size; i++) {
            String tbl = Text.readString(in);
            String alias = Text.readString(in);
            tblAlias.put(tbl, alias);
        }
    }

    @Override
    public String toString() {
        return toJson(true).toString();
    }
}

