global class MergeAgencyActivityBatch implements Database.Batchable<sObject>, Database.Stateful {
    global Map<Id, Contact> oldIdNewContactMap = new Map<Id, Contact>();    // 7. Agency_Report_Header 和 8. Agency_Report 里用到
    global Map<Id, Id>       oldIdNewUserIdMap = new Map<Id, Id>();         // Agency_XXX table Owner 設定用
    global Map<Id, Id>       newIdOldUserIdMap = new Map<Id, Id>();         // Agency_XXX table Owner 設定用
    global Map<Id, Agency_Hospital_Link__c> newAgencyHospitalMap = new Map<Id, Agency_Hospital_Link__c>(); // .客户人员(new, key is Hospital__c)
    
    public String       oldAgencyCode;
    public String       newAgencyCode;
    public Boolean      forceFlg;
    public List<String> emailMessages;
    public String       oldAgencyId;
    public String       newAgencyId;

    global Integer totalCount = 0; // 总件数
    global Integer failedCount = 0;

    @TestVisible
    private static List<String> messagesForTest;

    /**
     * @param forceFlg true:不对有没有New Agency的数据进行check
     */
    global MergeAgencyActivityBatch(String oldAgencyCode, String newAgencyCode, Boolean forceFlg) {
        this.oldAgencyCode = oldAgencyCode;
        this.newAgencyCode = newAgencyCode;
        this.forceFlg      = forceFlg;
        this.emailMessages = new List<String>();
    }

    // TODO katsu 暫定 1万 超えない前提
    global Database.QueryLocator start(Database.BatchableContext bc) {
        Account[] accList;
        accList = [SELECT Id
                FROM Account
                WHERE Management_Code__c = :this.oldAgencyCode];
        if (accList.size() != 1) {
            this.emailMessages.add('没有找到 或 有多条 旧经销商 [' + this.oldAgencyCode + '] 的数据, size=' + accList.size());
            return Database.getQueryLocator([SELECT Id FROM Agency_Hospital_Link__c LIMIT 0]);
        } else {
            this.oldAgencyId = accList[0].Id;
        }
        accList = [SELECT Id
                FROM Account
                WHERE Management_Code__c = :this.newAgencyCode];
        if (accList.size() != 1) {
            this.emailMessages.add('没有找到 或 有多条 新经销商 [' + this.newAgencyCode + '] 的数据, size=' + accList.size());
            return Database.getQueryLocator([SELECT Id FROM Agency_Hospital_Link__c LIMIT 0]);
        } else {
            this.newAgencyId = accList[0].Id;
        }
        if (forceFlg == false && [SELECT Id
                FROM Agency_Contact__c
                WHERE Agency_Hospital__r.Agency__r.Management_Code__c = :this.newAgencyCode].size() > 0) {

            this.emailMessages.add('.客户人员 里不可以有 New Agency [' + this.newAgencyCode + '] 的数据');
            return Database.getQueryLocator([SELECT Id FROM Agency_Hospital_Link__c LIMIT 0]);
        }
        if (forceFlg == false && [SELECT Id
                FROM Agency_Opportunity__c
                WHERE Agency_Hospital_All__r.Agency__r.Management_Code__c = :this.newAgencyCode].size() > 0) {

            this.emailMessages.add('经销商询价 里不可以有 New Agency [' + this.newAgencyCode + '] 的数据');
            return Database.getQueryLocator([SELECT Id FROM Agency_Hospital_Link__c LIMIT 0]);
        }
        if (forceFlg == false && [SELECT Id
                FROM Agency_Report_Header__c
                WHERE Agency__r.Management_Code__c = :this.newAgencyCode].size() > 0) {

            this.emailMessages.add('.周报一览 里不可以有 New Agency [' + this.newAgencyCode + '] 的数据');
            return Database.getQueryLocator([SELECT Id FROM Agency_Hospital_Link__c LIMIT 0]);
        }
        if (forceFlg == false && [SELECT Id
                FROM Agency_Report__c
                WHERE Agency__r.Management_Code__c = :this.newAgencyCode].size() > 0) {

            this.emailMessages.add('.周报明细 里不可以有 New Agency [' + this.newAgencyCode + '] 的数据');
            return Database.getQueryLocator([SELECT Id FROM Agency_Hospital_Link__c LIMIT 0]);
        }

        // 事前のデータを取得
        // 1. New Agency_Hospital 新的营销关系
        for (Agency_Hospital_Link__c nObj : [SELECT Id
                    , Agency__c, Agency__r.Management_Code__c, Hospital__c
                FROM Agency_Hospital_Link__c
                WHERE Agency__r.Management_Code__c = :this.newAgencyCode
                ORDER BY CreatedDate]) {
            // map的重复不用考虑
            newAgencyHospitalMap.put(nObj.Hospital__c, nObj);
        }

        // 5. New Contact
        String Contact_Agency_RT = Schema.SObjectType.Contact.getRecordTypeInfosByDeveloperName().get('Agency').getRecordTypeId();
        Map<Id, Contact>         newContactMap = new Map<Id, Contact>();     // 取引先責任者(new)
        Map<String, Contact> newContactNameMap = new Map<String, Contact>(); // 取引先責任者(new, key is Name)
        for (Contact nObj : [SELECT Id
                    , AccountId, Name, Agency_User__c
                FROM Contact
                WHERE RecordTypeId = :Contact_Agency_RT
                  AND Account.Management_Code__c = :this.newAgencyCode]) {

            // Contact.Name重复 check
            Contact oObj = newContactNameMap.get(nObj.Name);
            if (oObj != null) {
                this.emailMessages.add('客户人员 ' + oObj.Name + ' 重复, 请确认 ' + oObj.Id + ' , ' + nObj.Id + ' 的数据');
                return Database.getQueryLocator([SELECT Id FROM Agency_Hospital_Link__c LIMIT 0]);
            }
            newContactMap.put(nObj.Id, nObj);
            newContactNameMap.put(nObj.Name, nObj);
        }

        // 6. Old Contact
        Map<Id, Contact>         oldContactMap = new Map<Id, Contact>();
        Map<String, Contact> oldContactNameMap = new Map<String, Contact>(); // 取引先責任者(new, key is Name)
        for (Contact nObj : [SELECT Id, Name
                    , Account.Management_Code__c, Agency_User__c, FirstName, LastName
                FROM Contact
                WHERE RecordTypeId = :Contact_Agency_RT
                  AND Agency_User__c = true
                  AND Account.Management_Code__c = :this.oldAgencyCode]) {

            // Contact.Name重复 check
            Contact oObj = oldContactNameMap.get(nObj.Name);
            if (oObj != null) {
                this.emailMessages.add('客户人员 ' + oObj.Name + ' 重复, 请确认 ' + oObj.Id + ' , ' + nObj.Id + ' 的数据');
                return Database.getQueryLocator([SELECT Id FROM Agency_Hospital_Link__c LIMIT 0]);
            }
            oldContactMap.put(nObj.Id, nObj);
            oldContactNameMap.put(nObj.Name, nObj);
        }

        // 新旧UserのMapping
        Map<String, User> newUserNameMap = new Map<String, User>();
        for (User nObj : [SELECT Id, Name
                    , ContactId
                 FROM User
                WHERE IsPortalEnabled = true
                  AND IsActive = true
                  AND Contact.AccountId = :this.newAgencyId
                  AND ContactId IN :newContactMap.keySet()]) {
            newUserNameMap.put(nObj.Name, nObj);
        }
        Map<String, User> oldUserNameMap = new Map<String, User>();
        for (User nObj : [SELECT Id, Name
                    , ContactId
                 FROM User
                WHERE IsPortalEnabled = true
                  AND IsActive = true
                  AND Contact.AccountId = :this.oldAgencyId
                  AND ContactId IN :oldContactMap.keySet()]) {
            oldUserNameMap.put(nObj.Name, nObj);
        }

        // 5. New User 件数必须和 6 一样(force y也不让过)
        for (User oObj : oldUserNameMap.values()) {
            User nObj = newUserNameMap.get(oObj.Name);
            if (nObj == null) {
                this.emailMessages.add('User:' + oObj.Name + ' 在 ' + newAgencyCode + ' 里不存在, 请确认有没有造新的用户');
                return Database.getQueryLocator([SELECT Id FROM Agency_Hospital_Link__c LIMIT 0]);
            }
            this.oldIdNewUserIdMap.put(oObj.Id, nObj.Id);
            this.newIdOldUserIdMap.put(nObj.Id, oObj.Id);
        }
        if (this.oldIdNewUserIdMap.size() == 0) {
            this.emailMessages.add('没有找到 新经销商的用户数据');
            return Database.getQueryLocator([SELECT Id FROM Agency_Hospital_Link__c LIMIT 0]);
        }

        // 5. New Contact 件数必须和 6 一样(补到 一样 or 超过)
        List<Contact> upsList = new List<Contact>();
        for (Contact oObj : oldContactMap.values()) {
            Contact nObj = newContactNameMap.get(oObj.Name);
            Boolean isTarget = false;
            if (nObj == null) {
                nObj = new Contact(Agency_User__c = true
                    , RecordTypeId = Contact_Agency_RT
                    , FirstName = oObj.FirstName
                    , LastName = oObj.LastName
                );
                isTarget = true;
            }
            if (nObj.Agency_User__c != true) {
                isTarget = true;
            }
            nObj.AccountId = this.newAgencyId;
            nObj.Agency_User__c = true;
            this.oldIdNewContactMap.put(oObj.Id, nObj);
            if (isTarget) {
                upsList.add(nObj);
            }
        }
        List<Database.UpsertResult> urList = Database.upsert(upsList, Contact.Fields.Id, false);
        Integer errorCnt = setUpstError(urList, Contact.getSObjectType(), upsList);
        totalCount += upsList.size();
        if (errorCnt > 0) {
            failedCount += errorCnt;
            return Database.getQueryLocator([SELECT Id FROM Agency_Hospital_Link__c LIMIT 0]);
        }

        // 7. Agency_Report_Header
        List<Agency_Report_Header__c> oldArhList = [SELECT Id, Name
                    , Old_OwnerId__c, Old_Agency__c, Old_Agency_Person2__c
                    , OwnerId, Agency__c, Agency_Person2__c
                FROM Agency_Report_Header__c
                WHERE Agency__r.Management_Code__c = :this.oldAgencyCode];
        for (Agency_Report_Header__c ups : oldArhList) {
            if (false == newIdOldUserIdMap.containsKey(ups.OwnerId)) {
                ups.Old_OwnerId__c        = ups.OwnerId;
                ups.OwnerId               = oldIdNewUserIdMap.get(ups.OwnerId);
            }
            if (ups.Agency__c != this.newAgencyId) {
                ups.Old_Agency__c         = ups.Agency__c;
                ups.Agency__c             = this.newAgencyId;
                ups.Agency_ID__c          = this.newAgencyId.mid(0, 15);
            }
            if (ups.Agency_Person2__c != this.oldIdNewContactMap.get(ups.Agency_Person2__c).Id) {
                ups.Old_Agency_Person2__c = ups.Agency_Person2__c;
                ups.Agency_Person2__c     = this.oldIdNewContactMap.get(ups.Agency_Person2__c).Id;
            }
        }
        List<Database.SaveResult> srList = Database.update(oldArhList, false);
        AgencyReportHeaderHandler.shareAgency_Report_Header_ToRole(oldArhList);
        errorCnt = setSaveError(srList, Agency_Report_Header__c.getSObjectType(), oldArhList);
        totalCount += oldArhList.size();
        if (errorCnt > 0) {
            failedCount += errorCnt;
            return Database.getQueryLocator([SELECT Id FROM Agency_Hospital_Link__c LIMIT 0]);
        }

        // Batch実行
        // 2. Old Agency_Hospital 为基础
        return Database.getQueryLocator([SELECT Id
                    , Agency__c, Agency__r.Management_Code__c, Hospital__c, Hospital__r.Management_Code__c
                FROM Agency_Hospital_Link__c
                WHERE Agency__r.Management_Code__c = :this.oldAgencyCode
            ]
        );
    }

    global void execute(Database.BatchableContext BC, List<Agency_Hospital_Link__c> oldAgencyHospitalList) {
        Map<Id, Agency_Opportunity__c> oldAgencyOpportunityMap = new Map<Id, Agency_Opportunity__c>();   // 经销商询价(old)
        Map<Id, User> ocsmUserMap = new Map<Id, User>(); //ocsm内部ユーザー

        // 事前のデータを取得
        // 1. ocsm内部ユーザー
        ocsmUserMap = new Map<Id, User>([SELECT Id FROM User WHERE IsPortalEnabled = false]);
        // 2. Old Agency_Hospital 旧的营销关系 -> 新的营销关系
        Map<Id, Agency_Hospital_Link__c>  oldIdNewAgencyHospitalMap = new Map<Id, Agency_Hospital_Link__c>(); // 3. Agency_Contact, 4. Agency_Opportunity, 8. Agency_Report 里用到
        for (Agency_Hospital_Link__c oObj : oldAgencyHospitalList) {
            Agency_Hospital_Link__c nObj = this.newAgencyHospitalMap.get(oObj.Hospital__c);
            if (nObj == null) {
                this.emailMessages.add('没有找到新的营销关系 请确认是否存在 ' + oObj.Hospital__r.Management_Code__c + ' 的数据');
            } else {
                oldIdNewAgencyHospitalMap.put(oObj.Id, nObj);
            }
        }

        // 3. Agency_Contact
        List<Agency_Contact__c> oldAgencyContactList = [SELECT Id, Name
                    , Old_OwnerId__c, Old_Agency_Hospital__c, Old_Agency_ID__c
                    , OwnerId, Agency_Hospital__c, Agency_ID__c
                FROM Agency_Contact__c
                WHERE Agency_Hospital__c IN :oldAgencyHospitalList];
        for (Agency_Contact__c ups : oldAgencyContactList) {
            if (false == newIdOldUserIdMap.containsKey(ups.OwnerId)) {
                ups.Old_OwnerId__c        = ups.OwnerId;
                ups.OwnerId               = oldIdNewUserIdMap.get(ups.OwnerId);
            }
            if (ups.Agency_Hospital__c != oldIdNewAgencyHospitalMap.get(ups.Agency_Hospital__c).Id) {
                ups.Old_Agency_Hospital__c  = ups.Agency_Hospital__c;
                ups.Agency_Hospital__c      = oldIdNewAgencyHospitalMap.get(ups.Agency_Hospital__c).Id;
            }
            if (ups.Agency_ID__c != this.newAgencyId.mid(0, 15)) {
                ups.Old_Agency_ID__c        = ups.Agency_ID__c;
                ups.Agency_ID__c            = this.newAgencyId.mid(0, 15);
            }
        }
        List<Database.SaveResult> srList = Database.update(oldAgencyContactList, false);
        AgencyContactHandler.shareAgency_Contact_ToRole(oldAgencyContactList);
        failedCount += setSaveError(srList, Agency_Contact__c.getSObjectType(), oldAgencyContactList);
        totalCount += oldAgencyContactList.size();

        // 4. Agency_Opportunity
        List<Agency_Opportunity__c> oldAgencyOpportunityList = [SELECT Id, Name
                    , Old_OwnerId__c, Old_Agency__c, Old_Agency_Hospital__c, Old_Agency_Hospital_All__c, Old_Agency_Hospital_Target__c, Old_TargetInputKey__c
                    , OwnerId, Agency__c, Agency_ID__c, Agency_Hospital__c, Agency_Hospital_All__c, Agency_Hospital_Target__c, TargetInputKey__c
                FROM Agency_Opportunity__c
                WHERE Agency_Hospital_All__c IN :oldAgencyHospitalList];
        for (Agency_Opportunity__c ups : oldAgencyOpportunityList) {
            if (false == newIdOldUserIdMap.containsKey(ups.OwnerId)) {
                if (true == ocsmUserMap.containsKey(ups.OwnerId)) {
                    //ocsm内部ユーザー：更新しない  
                } else {
                    ups.Old_OwnerId__c        = ups.OwnerId;
                    ups.OwnerId               = oldIdNewUserIdMap.get(ups.OwnerId);
                }
            }
            if (ups.Agency__c != this.newAgencyId) {
                ups.Old_Agency__c               = ups.Agency__c;
                ups.Agency__c                   = this.newAgencyId;
                ups.Agency_ID__c                = this.newAgencyId.mid(0, 15);
            }
            if (String.isNotBlank(ups.Agency_Hospital__c)
                    && ups.Agency_Hospital__c != oldIdNewAgencyHospitalMap.get(ups.Agency_Hospital__c).Id) {
                ups.Old_Agency_Hospital__c      = ups.Agency_Hospital__c;
                ups.Agency_Hospital__c          = oldIdNewAgencyHospitalMap.get(ups.Agency_Hospital__c).Id;
            }
            if (String.isNotBlank(ups.Agency_Hospital_All__c)
                    && ups.Agency_Hospital_All__c != oldIdNewAgencyHospitalMap.get(ups.Agency_Hospital_All__c).Id) {
                ups.Old_Agency_Hospital_All__c  = ups.Agency_Hospital_All__c;
                ups.Agency_Hospital_All__c      = oldIdNewAgencyHospitalMap.get(ups.Agency_Hospital_All__c).Id;
            }
            if (String.isNotBlank(ups.Agency_Hospital_Target__c)
                    && ups.Agency_Hospital_Target__c != oldIdNewAgencyHospitalMap.get(ups.Agency_Hospital_Target__c).Id) {
                ups.Old_Agency_Hospital_Target__c = ups.Agency_Hospital_Target__c;
                ups.Agency_Hospital_Target__c   = oldIdNewAgencyHospitalMap.get(ups.Agency_Hospital_Target__c).Id;
                ups.Old_TargetInputKey__c       = ups.TargetInputKey__c;
                ups.TargetInputKey__c           = ups.Agency_Hospital_Target__c + ups.TargetInputKey__c.mid(18, 1300);
            }
        }
        srList = Database.update(oldAgencyOpportunityList, false);
        AgencyOpportunityHandler.shareAgency_Opportunity_ToRole(oldAgencyOpportunityList);
        failedCount += setSaveError(srList, Agency_Opportunity__c.getSObjectType(), oldAgencyOpportunityList);
        totalCount += oldAgencyOpportunityList.size();

        // 8. Agency_Report
        List<Agency_Report__c> oldAgencyReportList = [SELECT Id, Name
                    , Old_Agency__c, Old_Agency_Hospital__c, Old_Person_In_Charge2__c
                    , Agency__c, Agency_ID__c, Agency_Hospital__c, Person_In_Charge2__c
                FROM Agency_Report__c
                WHERE Agency__r.Management_Code__c = :this.oldAgencyCode
                  AND Agency_Hospital__c IN :oldAgencyHospitalList];
        for (Agency_Report__c ups : oldAgencyReportList) {
            if (ups.Agency__c != this.newAgencyId) {
                ups.Old_Agency__c               = ups.Agency__c;
                ups.Agency__c                   = this.newAgencyId;
                ups.Agency_ID__c                = this.newAgencyId.mid(0, 15);
            }
            if (String.isNotBlank(ups.Agency_Hospital__c)
                    && ups.Agency_Hospital__c != oldIdNewAgencyHospitalMap.get(ups.Agency_Hospital__c).Id) {
                ups.Old_Agency_Hospital__c      = ups.Agency_Hospital__c;
                ups.Agency_Hospital__c          = oldIdNewAgencyHospitalMap.get(ups.Agency_Hospital__c).Id;
            }
            if (ups.Person_In_Charge2__c != this.oldIdNewContactMap.get(ups.Person_In_Charge2__c).Id) {
                ups.Old_Person_In_Charge2__c = ups.Person_In_Charge2__c;
                ups.Person_In_Charge2__c     = this.oldIdNewContactMap.get(ups.Person_In_Charge2__c).Id;
            }
        }
        srList = Database.update(oldAgencyReportList, false);
        failedCount += setSaveError(srList, Agency_Report__c.getSObjectType(), oldAgencyReportList);
        totalCount += oldAgencyReportList.size();
    }

    global void finish(Database.BatchableContext BC) {
        MergeAgencyActivityBatch.messagesForTest = this.emailMessages;
        // 发 mail
        BatchEmailUtil eb = new BatchEmailUtil();
        String title = '经销商用户更名 ' + this.oldAgencyCode + ' -> ' + this.newAgencyCode;
        String ccStr = null;
        if(this.emailMessages.size() == 0){
            eb.successMail(title, totalCount);
        }else{
            eb.failedMail(title, String.join(this.emailMessages, '\n'),
                totalCount, totalCount - failedCount, failedCount);
        }
        eb.send();
    }

    @TestVisible
    private Integer setUpstError(List<Database.UpsertResult> srList, Schema.sObjectType obj
            , List<SObject> upsList) {
        Integer errorCnt = 0;
        List<String> errMsgList = new List<String>();
        String objectName = obj.getDescribe().getName();
        String objectLabel = obj.getDescribe().getLabel();
        for (Integer i = 0; i < srList.size(); i++) {
            SObject eObj = upsList[i];
            Database.UpsertResult sr = srList[i];
            if (!sr.isSuccess()) {
                errorCnt++;
                for (Database.Error err : sr.getErrors()) {
                    String message = objectLabel + ':'
                            + objectName + '=' + eObj.get('Name')
                            + ', ' + err.getStatusCode() + ':' + err.getMessage()
                            + ((err.getFields() != null && err.getFields().size() > 0) ? ', Field:' + String.valueOf(err.getFields()) : '');
                    this.emailMessages.add(message);
                }
            }
        }
        return errorCnt;
    }

    @TestVisible
    private Integer setSaveError(List<Database.SaveResult> srList, Schema.sObjectType obj
            , List<SObject> upsList) {
        Integer errorCnt = 0;
        String objectName = obj.getDescribe().getName();
        String objectLabel = obj.getDescribe().getLabel();
        for (Integer i = 0; i < srList.size(); i++) {
            SObject eObj = upsList[i];
            Database.SaveResult sr = srList[i];
            if (!sr.isSuccess()) {
                errorCnt++;
                for (Database.Error err : sr.getErrors()) {
                    String message = objectLabel + ':'
                            + objectName + '=' + eObj.get('Name')
                            + ', ' + err.getStatusCode() + ':' + err.getMessage()
                            + ((err.getFields() != null && err.getFields().size() > 0) ? ', Field:' + String.valueOf(err.getFields()) : '');
                    this.emailMessages.add(message);
                }
            }
        }
        return errorCnt;
    }

    private static Map<Id, Id> accIdGrpIdMap;
    public static Id accIdGrpIdMap(Id accId) {
        if (MergeAgencyActivityBatch.accIdGrpIdMap == null) { MergeAgencyActivityBatch.accIdGrpIdMap = new Map<Id, Id>(); }
        if (MergeAgencyActivityBatch.accIdGrpIdMap.get(accId) == null) {
            List<UserRole> roles = [SELECT Id, PortalAccountId
                    FROM UserRole
                    WHERE PortalAccountId = :accId
                    AND PortalRole = 'Worker'];
            Map<Id, Id> roleIdAccId = new Map<Id, Id>();
            for (UserRole ur : roles) {
                roleIdAccId.put(ur.Id, ur.PortalAccountId);
            }
            List<Group> groups = [SELECT Id, RelatedId
                    FROM Group
                    WHERE RelatedId = :roleIdAccId.keySet()
                    AND Type = 'RoleAndSubordinates'];
            for (Group grp : groups) {
                MergeAgencyActivityBatch.accIdGrpIdMap.put(roleIdAccId.get(grp.RelatedId), grp.Id);
            }
        }
        return MergeAgencyActivityBatch.accIdGrpIdMap.get(accId);
    }
}