package core

import (
	"encoding/json"
	"fmt"
	"intent-system/pkg/config"
	"intent-system/pkg/dal/dao"
	"intent-system/pkg/dal/models"
	"intent-system/pkg/itypes"
	"intent-system/pkg/proto"
	"intent-system/pkg/utils"

	"github.com/civet148/log"
	"github.com/civet148/sqlca/v2"
)

type BizCore struct {
	db            *sqlca.Engine
	cfg           *config.Config
	tagDAO        *dao.TagDAO
	userDAO       *dao.UserDAO
	newsDAO       *dao.NewsDAO
	newsDraftDAO  *dao.NewsDraftDAO
	customerDAO   *dao.CustomerDAO
	dictionaryDAO *dao.DictionaryDAO
	subDAO        *dao.NewsSubscribeDAO
	qaDAO         *dao.QuestionAnswerDAO
	qaDraftDAO    *dao.QuestionDraftDAO
}

func NewBizCore(cfg *config.Config, db *sqlca.Engine) *BizCore {
	return &BizCore{
		db:            db,
		cfg:           cfg,
		tagDAO:        dao.NewTagDAO(db),
		userDAO:       dao.NewUserDAO(db),
		newsDAO:       dao.NewNewsDAO(db),
		newsDraftDAO:  dao.NewNewsDraftDAO(db),
		customerDAO:   dao.NewCustomerDAO(db),
		dictionaryDAO: dao.NewDictionaryDAO(db),
		qaDAO:         dao.NewQuestionAnswerDAO(db),
		subDAO:        dao.NewNewsSubscribeDAO(db),
		qaDraftDAO:    dao.NewQuestionDraftDAO(db),
	}
}

func (m *BizCore) NewsList(ctx *itypes.Context, req *proto.NewsListReq, needExtra bool) (resp *proto.NewsListResp, total int64, code itypes.BizCode) {
	var err error
	var dos []*models.NewsDO
	dos, total, err = m.newsDAO.QueryList(&dao.NewsCondition{
		PageNo:       req.PageNo,
		PageSize:     req.PageSize,
		Id:           req.Id,
		Tag:          req.Tag,
		All:          req.All,
		IsDeleted:    req.IsDeleted,
		ContainExtra: needExtra,
		Asc:          req.OrderAsc,
		Search:       req.Search,
		Language:     req.Language,
	})
	if err != nil {
		return nil, 0, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.NewsListResp{
		List: dos,
	}, total, itypes.BizOK
}

func (m *BizCore) NewsAdd(ctx *itypes.Context, req *proto.NewsAddReq) (resp *proto.NewsAddResp, code itypes.BizCode) {
	var id int64
	var err error
	logs := models.MakeChangeLog(nil, ctx.UserName(), models.OperType_Create)
	id, err = m.newsDraftDAO.Insert(&models.NewsDraftDO{
		NewsId:      0,
		OrgId:       0,
		Category:    req.Category,
		MainTitle:   req.MainTitle,
		SubTitle:    req.SubTitle,
		Summary:     req.Summary,
		Keywords:    utils.JsonMarshal(req.Keywords),
		SeoKeywords: utils.JsonMarshal(req.SeoKeywords),
		Tags:        req.Tags,
		ImageUrl:    req.ImageUrl,
		Content:     req.Content,
		IsReplicate: false,
		Language:    req.Language,
		ExtraData: models.CommonExtraData{
			Logs: logs,
		},
	})
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.NewsAddResp{
		DraftId: id,
	}, itypes.BizOK
}

func (m *BizCore) NewsEdit(ctx *itypes.Context, req *proto.NewsEditReq) (resp *proto.NewsEditResp, code itypes.BizCode) {
	var id int64
	var err error
	var news *models.NewsDO
	news, err = m.newsDAO.QueryById(req.Id)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	if news.Id == 0 {
		err = log.Errorf("news id [%v] not found", req.Id)
		return nil, itypes.NewBizCode(itypes.CODE_INVALID_PARAMS, err.Error())
	}
	logs := models.MakeChangeLog(nil, ctx.UserName(), models.OperType_Create)

	id, err = m.newsDraftDAO.Upsert(&models.NewsDraftDO{
		NewsId:      news.Id,
		OrgId:       news.OrgId,
		Content:     news.Content,
		Category:    news.Category,
		MainTitle:   news.MainTitle,
		SubTitle:    news.SubTitle,
		Summary:     news.Summary,
		Keywords:    news.Keywords,
		SeoKeywords: news.SeoKeywords,
		Tags:        news.Tags,
		ImageUrl:    news.ImageUrl,
		IsReplicate: news.IsReplicate,
		Language:    news.Language,
		ExtraData: models.CommonExtraData{
			Logs: logs,
		},
	},
		models.NEWS_DRAFT_COLUMN_CATEGORY,
		models.NEWS_DRAFT_COLUMN_MAIN_TITLE,
		models.NEWS_DRAFT_COLUMN_SUB_TITLE,
		models.NEWS_DRAFT_COLUMN_SUMMARY,
		models.NEWS_DRAFT_COLUMN_KEYWORDS,
		models.NEWS_DRAFT_COLUMN_SEO_KEYWORDS,
		models.NEWS_DRAFT_COLUMN_TAGS,
		models.NEWS_DRAFT_COLUMN_IMAGE_URL,
		models.NEWS_DRAFT_COLUMN_CONTENT,
		models.NEWS_DRAFT_COLUMN_LANGUAGE,
		models.NEWS_DRAFT_COLUMN_IS_DELETED,
		models.NEWS_DRAFT_COLUMN_IS_REPLICATE,
		models.NEWS_DRAFT_COLUMN_EXTRA_DATA,
	)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	// news.ExtraData.Logs = models.MakeChangeLog(news.ExtraData.Logs, ctx.UserName(), models.OperType_Edit)
	var prevLogs []*models.ChangeLog
	if raw, ok := news.ExtraData["logs"]; ok {
		if arr, ok := raw.([]interface{}); ok {
			// 先 marshal 回去再 unmarshal 成 []*ChangeLog（通用做法）
			bytes, _ := json.Marshal(arr)
			_ = json.Unmarshal(bytes, &prevLogs)
		}
	}
	news.ExtraData["logs"] = models.MakeChangeLog(prevLogs, ctx.UserName(), models.OperType_Edit)
	_, err = m.newsDAO.Update(&models.NewsDO{
		Id:        news.Id,
		ExtraData: news.ExtraData,
	}, models.NEWS_COLUMN_EXTRA_DATA)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.NewsEditResp{
		DraftId: id,
	}, itypes.BizOK
}

func (m *BizCore) NewsDelete(ctx *itypes.Context, req *proto.NewsDeleteReq) (resp *proto.NewsDeleteResp, code itypes.BizCode) {
	var err error
	var news *models.NewsDO
	for _, id := range req.Ids {
		news, err = m.newsDAO.QueryById(id)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
		if news.Id == 0 {
			err = log.Errorf("news id [%v] not found", id)
			return nil, itypes.NewBizCode(itypes.CODE_INVALID_PARAMS, err.Error())
		}

		// news.ExtraData.Logs = models.MakeChangeLog(news.ExtraData.Logs, ctx.UserName(), models.OperType_Delete)
		var prevLogs []*models.ChangeLog
		if raw, ok := news.ExtraData["logs"]; ok {
			if arr, ok := raw.([]interface{}); ok {
				// 尝试转成 []*ChangeLog
				bytes, _ := json.Marshal(arr)
				_ = json.Unmarshal(bytes, &prevLogs)
			}
		}
		news.ExtraData["logs"] = models.MakeChangeLog(prevLogs, ctx.UserName(), models.OperType_Delete)

		_, err = m.newsDAO.Update(&models.NewsDO{
			Id:        id,
			IsDeleted: true,
			ExtraData: news.ExtraData,
		}, models.NEWS_COLUMN_IS_DELETED, models.NEWS_COLUMN_EXTRA_DATA)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
	}

	return &proto.NewsDeleteResp{}, itypes.BizOK
}

func (m *BizCore) NewsCompare(ctx *itypes.Context, req *proto.NewsCompareReq) (resp *proto.NewsCompareResp, code itypes.BizCode) {
	curNews, err := m.newsDAO.QueryById(req.Id)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	if !curNews.IsReplicate {
		return &proto.NewsCompareResp{
			CurNews: curNews,
			OrgNews: &models.NewsDO{},
		}, itypes.BizOK
	}
	var orgNews *models.NewsDO
	orgNews, err = m.newsDAO.QueryOriginalNews(curNews.OrgId, curNews.Language)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.NewsCompareResp{
		CurNews: curNews,
		OrgNews: orgNews,
	}, itypes.BizOK
}

func (m *BizCore) NewsDraftList(ctx *itypes.Context, req *proto.NewsDraftListReq) (resp *proto.NewsDraftListResp, total int64, code itypes.BizCode) {
	var err error
	var dos []*models.NewsDraftDO
	dos, total, err = m.newsDraftDAO.QueryList(&dao.NewsDraftCondition{
		PageNo:   req.PageNo,
		PageSize: req.PageSize,
		Asc:      req.OrderAsc,
		Id:       req.Id,
		Search:   req.Search,
	})
	if err != nil {
		return nil, 0, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.NewsDraftListResp{
		List: dos,
	}, total, itypes.BizOK
}

func (m *BizCore) NewsDraftEdit(ctx *itypes.Context, req *proto.NewsDraftEditReq) (resp *proto.NewsDraftEditResp, code itypes.BizCode) {
	var columns = []string{
		models.NEWS_DRAFT_COLUMN_CATEGORY,
		models.NEWS_DRAFT_COLUMN_MAIN_TITLE,
		models.NEWS_DRAFT_COLUMN_SUB_TITLE,
		models.NEWS_DRAFT_COLUMN_SUMMARY,
		models.NEWS_DRAFT_COLUMN_KEYWORDS,
		models.NEWS_DRAFT_COLUMN_SEO_KEYWORDS,
		models.NEWS_DRAFT_COLUMN_TAGS,
		models.NEWS_DRAFT_COLUMN_IMAGE_URL,
		models.NEWS_DRAFT_COLUMN_CONTENT,
	}
	if req.Language != "" {
		columns = append(columns, models.NEWS_DRAFT_COLUMN_LANGUAGE)
	}
	_, err := m.newsDraftDAO.Update(&models.NewsDraftDO{
		Id:          req.Id,
		Category:    req.Category,
		MainTitle:   req.MainTitle,
		SubTitle:    req.SubTitle,
		Summary:     req.Summary,
		Keywords:    utils.JsonMarshal(req.Keywords),
		SeoKeywords: utils.JsonMarshal(req.SeoKeywords),
		Tags:        req.Tags,
		ImageUrl:    req.ImageUrl,
		Content:     req.Content,
		Language:    req.Language,
		ExtraData:   models.CommonExtraData{},
	}, columns...)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.NewsDraftEditResp{}, itypes.BizOK
}

func (m *BizCore) NewsDraftPublish(ctx *itypes.Context, req *proto.NewsDraftPublishReq) (resp *proto.NewsDraftPublishResp, code itypes.BizCode) {
	draft, err := m.newsDraftDAO.QueryById(req.Id)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	if draft.Id == 0 || draft.IsDeleted {
		return nil, itypes.NewBizCode(itypes.CODE_INVALID_PARAMS, "news draft id invalid or draft is deleted")
	}
	if draft.NewsId == 0 {
		_, err = m.newsDAO.Insert(&models.NewsDO{
			OrgId:       draft.OrgId,
			Category:    draft.Category,
			MainTitle:   draft.MainTitle,
			SubTitle:    draft.SubTitle,
			Summary:     draft.Summary,
			Keywords:    draft.Keywords,
			SeoKeywords: draft.SeoKeywords,
			Tags:        draft.Tags,
			ImageUrl:    draft.ImageUrl,
			Content:     draft.Content,
			Language:    draft.Language,
			// ExtraData:   models.CommonExtraData{},
			ExtraData: map[string]interface{}{}, // ✅ 替代旧结构体
		})
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
	} else {
		var news *models.NewsDO
		news, err = m.newsDAO.QueryById(draft.NewsId)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
		if news.Id == 0 {
			err = log.Errorf("news id [%d] not found", draft.NewsId)
			return nil, itypes.NewBizCode(itypes.CODE_INVALID_PARAMS, err.Error())
		}
		_, err = m.newsDAO.Update(&models.NewsDO{
			Id:          draft.NewsId,
			Category:    draft.Category,
			MainTitle:   draft.MainTitle,
			SubTitle:    draft.SubTitle,
			Summary:     draft.Summary,
			Keywords:    draft.Keywords,
			SeoKeywords: draft.SeoKeywords,
			Tags:        draft.Tags,
			ImageUrl:    draft.ImageUrl,
			Content:     draft.Content,
			Language:    draft.Language,
			// ExtraData:   models.CommonExtraData{},
			ExtraData: map[string]interface{}{}, // ✅ 替代旧结构体
		}, models.NEWS_COLUMN_CONTENT, models.NEWS_COLUMN_LANGUAGE, models.NEWS_COLUMN_EXTRA_DATA)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
		if news.OrgId != 0 { //is a news replicate from AI news, just set overwritten flag
			_, err = m.newsDAO.UpdateByOrgId(&models.NewsDO{
				OrgId:         draft.OrgId,
				IsOverwritten: true,
			}, models.NEWS_COLUMN_IS_OVERWRITTEN)
			if err != nil {
				return nil, itypes.NewBizCodeDatabaseError(err.Error())
			}
		}
	}
	_, err = m.newsDraftDAO.Update(&models.NewsDraftDO{
		Id:        draft.Id,
		IsDeleted: true,
	}, models.NEWS_DRAFT_COLUMN_IS_DELETED)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.NewsDraftPublishResp{}, itypes.BizOK
}

func (m *BizCore) NewsDraftDelete(ctx *itypes.Context, req *proto.NewsDraftDeleteReq) (resp *proto.NewsDraftDeleteResp, code itypes.BizCode) {
	for _, id := range req.Ids {
		_, err := m.newsDraftDAO.Update(&models.NewsDraftDO{
			Id:        id,
			IsDeleted: true,
			ExtraData: models.CommonExtraData{},
		}, models.NEWS_DRAFT_COLUMN_IS_DELETED)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
	}

	return &proto.NewsDraftDeleteResp{}, itypes.BizOK
}

func (m *BizCore) NewsTag(ctx *itypes.Context, req *proto.NewsTagReq) (resp *proto.NewsTagResp, code itypes.BizCode) {
	news, err := m.newsDAO.QueryById(req.Id)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	news.Tags = req.Tags
	_, err = m.newsDAO.Update(news, models.NEWS_COLUMN_TAGS)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.NewsTagResp{}, itypes.BizOK
}

func (m *BizCore) NewsPublish(ctx *itypes.Context, req *proto.NewsPublishReq) (resp *proto.NewsPublishResp, code itypes.BizCode) {
	var err error
	var subNewses []*models.NewsDO
	subNewses, _, err = m.subDAO.QueryTodayNewsList(0, 0)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	for _, s := range subNewses {
		s.State = dao.NewsState_NotPublish
		_, err = m.newsDAO.Update(s, models.NEWS_COLUMN_STATE)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
	}
	news, err := m.newsDAO.QueryById(req.Id)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	if news.Id == 0 {
		err = log.Errorf("news id [%v] not found", req.Id)
		return nil, itypes.NewBizCode(itypes.CODE_INVALID_PARAMS, err.Error())
	}
	var subNews *models.NewsSubscribeDO
	subNews, err = m.subDAO.QueryByNewsId(req.Id)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	subNews.NewsId = news.Id
	subNews.NewsUrl = m.makePublishUrl(news)
	subNews.NewsSubject = news.MainTitle
	subNews.IsDeleted = false
	if subNews.Id == 0 {
		err = m.subDAO.DeleteNotPushed()
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
		_, err = m.subDAO.Insert(subNews)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
	} else {
		if subNews.IsPushed {
			err = log.Errorf("news id [%v] subject [%s] url [%s] is already pushed", subNews.NewsId, subNews.NewsSubject, subNews.NewsUrl)
			return nil, itypes.NewBizCode(itypes.CODE_INVALID_PARAMS, err.Error())
		}
		_, err = m.subDAO.Update(subNews,
			models.NEWS_SUBSCRIBE_COLUMN_NEWS_URL,
			models.NEWS_SUBSCRIBE_COLUMN_NEWS_SUBJECT,
			models.NEWS_SUBSCRIBE_COLUMN_NEWS_URL,
			models.NEWS_SUBSCRIBE_COLUMN_IS_DELETED)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
	}
	news.State = dao.NewsState_Published
	_, err = m.newsDAO.Update(news, models.NEWS_COLUMN_STATE)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.NewsPublishResp{}, itypes.BizOK
}

func (m *BizCore) makePublishUrl(news *models.NewsDO) string {
	return fmt.Sprintf("%s/%v", m.cfg.Domain, news.Id)
}

func (m *BizCore) QaList(ctx *itypes.Context, req *proto.QaListReq, needExtra bool) (resp *proto.QaListResp, total int64, code itypes.BizCode) {
	var err error
	var dos []*models.QuestionAnswerDO

	dos, total, err = m.qaDAO.QueryList(&dao.QuestionAnswerCondition{
		PageNo:       req.PageNo,
		PageSize:     req.PageSize,
		Id:           req.Id,
		IsDeleted:    req.IsDeleted,
		ContainExtra: needExtra,
		Asc:          req.OrderAsc,
		Search:       req.Search,
		Language:     req.Language,
	})
	if err != nil {
		err = log.Errorf(err.Error())
		return nil, 0, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.QaListResp{
		List: dos,
	}, total, itypes.BizOK
}

func (m *BizCore) QaAdd(ctx *itypes.Context, req *proto.QaAddReq) (resp *proto.QaAddResp, code itypes.BizCode) {
	logs := models.MakeChangeLog(nil, ctx.UserName(), models.OperType_Create)

	id, err := m.qaDraftDAO.Insert(&models.QuestionDraftDO{
		QaId:          0,
		Question:      req.Question,
		Answer:        req.Answer,
		IsOverwritten: false,
		IsReplicate:   false,
		IsDeleted:     false,
		Language:      req.Language,
		ExtraData: models.CommonExtraData{
			Logs: logs,
		},
	})
	if err != nil {
		err = log.Errorf(err.Error())
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.QaAddResp{
		DraftId: id,
	}, itypes.BizOK
}

func (m *BizCore) QaEdit(ctx *itypes.Context, req *proto.QaEditReq) (resp *proto.QaEditResp, code itypes.BizCode) {
	var id int64
	var err error
	var qa *models.QuestionAnswerDO
	qa, err = m.qaDAO.QueryById(req.Id)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	if qa.Id == 0 {
		err = log.Errorf("Q&A id [%v] not found", req.Id)
		return nil, itypes.NewBizCode(itypes.CODE_INVALID_PARAMS, err.Error())
	}
	qa.ExtraData.Logs = models.MakeChangeLog(qa.ExtraData.Logs, ctx.UserName(), models.OperType_Edit)

	id, err = m.qaDraftDAO.Upsert(&models.QuestionDraftDO{
		QaId:        qa.Id,
		OrgId:       qa.OrgId,
		Question:    qa.Question,
		Answer:      qa.Answer,
		IsReplicate: qa.IsReplicate,
		ExtraData:   qa.ExtraData,
		Language:    qa.Language,
	},
		models.QUESTION_DRAFT_COLUMN_QUESTION,
		models.QUESTION_DRAFT_COLUMN_ANSWER,
		models.QUESTION_DRAFT_COLUMN_LANGUAGE,
	)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.QaEditResp{
		DraftId: id,
	}, itypes.BizOK
}

func (m *BizCore) QaDelete(ctx *itypes.Context, req *proto.QaDeleteReq) (resp *proto.QaDeleteResp, code itypes.BizCode) {
	var err error
	var qa *models.QuestionAnswerDO
	for _, id := range req.Ids {
		qa, err = m.qaDAO.QueryById(id)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
		if qa.Id == 0 {
			err = log.Errorf("Q&A id [%v] not found", id)
			return nil, itypes.NewBizCode(itypes.CODE_INVALID_PARAMS, err.Error())
		}
		qa.ExtraData.Logs = models.MakeChangeLog(qa.ExtraData.Logs, ctx.UserName(), models.OperType_Delete)
		_, err = m.qaDAO.Update(&models.QuestionAnswerDO{
			Id:        id,
			IsDeleted: true,
			ExtraData: qa.ExtraData,
		}, models.QUESTION_ANSWER_COLUMN_IS_DELETED, models.QUESTION_ANSWER_COLUMN_EXTRA_DATA)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
	}

	return &proto.QaDeleteResp{}, itypes.BizOK
}

func (m *BizCore) QaDraftList(ctx *itypes.Context, req *proto.QaDraftListReq) (resp *proto.QaDraftListResp, total int64, code itypes.BizCode) {
	var err error
	var dos []*models.QuestionDraftDO
	dos, total, err = m.qaDraftDAO.QueryList(&dao.QaDraftCondition{
		PageNo:   req.PageNo,
		PageSize: req.PageSize,
		Asc:      req.OrderAsc,
		Id:       req.Id,
		Search:   req.Search,
	})
	if err != nil {
		err = log.Errorf(err.Error())
		return nil, 0, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.QaDraftListResp{
		List: dos,
	}, total, itypes.BizOK
}

func (m *BizCore) QaDraftEdit(ctx *itypes.Context, req *proto.QaDraftEditReq) (resp *proto.QaDraftEditResp, code itypes.BizCode) {
	var err error
	var draft *models.QuestionDraftDO
	draft, err = m.qaDraftDAO.QueryById(req.Id)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	if draft.Id == 0 {
		err = log.Errorf("Q&A draft id [%v] not found", req.Id)
		return nil, itypes.NewBizCode(itypes.CODE_INVALID_PARAMS, err.Error())
	}
	var columns = []string{
		models.QUESTION_DRAFT_COLUMN_QUESTION,
		models.QUESTION_DRAFT_COLUMN_ANSWER,
		models.QUESTION_DRAFT_COLUMN_EXTRA_DATA,
	}
	if req.Language != "" {
		columns = append(columns, models.QUESTION_DRAFT_COLUMN_LANGUAGE)
	}
	draft.ExtraData.Logs = models.MakeChangeLog(draft.ExtraData.Logs, ctx.UserName(), models.OperType_Edit)
	_, err = m.qaDraftDAO.Update(&models.QuestionDraftDO{
		Id:        req.Id,
		Question:  req.Question,
		Answer:    req.Answer,
		ExtraData: draft.ExtraData,
		Language:  req.Language,
	}, columns...)
	if err != nil {
		err = log.Errorf(err.Error())
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.QaDraftEditResp{}, itypes.BizOK
}

func (m *BizCore) QaDraftPublish(ctx *itypes.Context, req *proto.QaDraftPublishReq) (resp *proto.QaDraftPublishResp, code itypes.BizCode) {
	draft, err := m.qaDraftDAO.QueryById(req.Id)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	if draft.Id == 0 {
		return nil, itypes.NewBizCode(itypes.CODE_INVALID_PARAMS)
	}
	if draft.QaId == 0 {
		_, err = m.qaDAO.Insert(&models.QuestionAnswerDO{
			OrgId:     draft.OrgId,
			Question:  draft.Question,
			Answer:    draft.Answer,
			ExtraData: models.CommonExtraData{},
		})
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
	} else {
		var qa *models.QuestionAnswerDO
		qa, err = m.qaDAO.QueryById(draft.QaId)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
		if qa.Id == 0 {
			err = log.Errorf("Q&A id [%d] not found", draft.QaId)
			return nil, itypes.NewBizCode(itypes.CODE_INVALID_PARAMS, err.Error())
		}
		_, err = m.qaDAO.Insert(&models.QuestionAnswerDO{
			OrgId:     draft.OrgId,
			Question:  draft.Question,
			Answer:    draft.Answer,
			ExtraData: models.CommonExtraData{},
		})
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
		if qa.OrgId != 0 { //is a news replicate from AI Q&A, just set overwritten flag
			_, err = m.qaDAO.Update(&models.QuestionAnswerDO{
				Id:            draft.QaId,
				IsOverwritten: true,
			}, models.NEWS_COLUMN_IS_OVERWRITTEN)
			if err != nil {
				return nil, itypes.NewBizCodeDatabaseError(err.Error())
			}
		}
	}

	_, err = m.qaDraftDAO.Update(&models.QuestionDraftDO{
		Id:        draft.Id,
		IsDeleted: true,
	}, models.QUESTION_DRAFT_COLUMN_IS_DELETED)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.QaDraftPublishResp{}, itypes.BizOK
}

func (m *BizCore) QaDraftDelete(ctx *itypes.Context, req *proto.QaDraftDeleteReq) (resp *proto.QaDraftDeleteResp, code itypes.BizCode) {
	var err error
	var draft *models.QuestionDraftDO
	for _, id := range req.Ids {
		draft, err = m.qaDraftDAO.QueryById(id)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
		if draft.Id == 0 {
			err = log.Errorf("Q&A draft id [%v] not found", id)
			return nil, itypes.NewBizCode(itypes.CODE_INVALID_PARAMS, err.Error())
		}
		draft.ExtraData.Logs = models.MakeChangeLog(draft.ExtraData.Logs, ctx.UserName(), models.OperType_Edit)
		_, err = m.qaDraftDAO.Update(&models.QuestionDraftDO{
			Id:        id,
			IsDeleted: true,
			ExtraData: draft.ExtraData,
		}, models.QUESTION_DRAFT_COLUMN_IS_DELETED)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
	}

	return &proto.QaDraftDeleteResp{}, itypes.BizOK
}

func (m *BizCore) SubListAll(ctx *itypes.Context, req *proto.SubListAllReq) (resp *proto.SubListAllResp, total int64, code itypes.BizCode) {
	var err error
	var dos []*models.NewsDO
	dos, total, err = m.subDAO.QueryAllNews(&dao.NewsSubscribeCondition{
		PageNo:   req.PageNo,
		PageSize: req.PageSize,
		Asc:      req.OrderAsc,
		Id:       req.Id,
		Search:   req.Search,
	})
	if err != nil {
		return nil, 0, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.SubListAllResp{
		List: dos,
	}, total, itypes.BizOK
}

func (m *BizCore) SubListPushed(ctx *itypes.Context, req *proto.SubListPushedReq) (resp *proto.SubListPushedResp, total int64, code itypes.BizCode) {
	var err error
	var dos []*models.NewsDO
	dos, total, err = m.subDAO.QueryPushedNews(&dao.NewsSubscribeCondition{
		PageNo:   req.PageNo,
		PageSize: req.PageSize,
		Asc:      req.OrderAsc,
		Id:       req.Id,
		Search:   req.Search,
	})
	if err != nil {
		return nil, 0, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.SubListPushedResp{
		List: dos,
	}, total, itypes.BizOK
}

func (m *BizCore) SubListToday(ctx *itypes.Context, req *proto.SubListTodayReq) (resp *proto.SubListTodayResp, total int64, code itypes.BizCode) {
	var err error
	var dos []*models.NewsDO
	dos, total, err = m.subDAO.QueryTodayNewsList(0, 1)
	if err != nil {
		return nil, 0, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.SubListTodayResp{
		List: dos,
	}, total, itypes.BizOK
}

func (m *BizCore) SubAddNews(ctx *itypes.Context, req *proto.SubAddNewsReq) (resp *proto.SubAddNewsResp, code itypes.BizCode) {
	return &proto.SubAddNewsResp{}, itypes.NewBizCode(itypes.CODE_ACCESS_DENY, "do not call this method")
}

func (m *BizCore) SubEditNews(ctx *itypes.Context, req *proto.SubEditNewsReq) (resp *proto.SubEditNewsResp, code itypes.BizCode) {
	ok, err := m.subDAO.IsSubNewsExist(req.Id)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	if !ok {
		err = log.Errorf("news id [%d] is not a subscribing news", req.Id)
		return nil, itypes.NewBizCode(itypes.CODE_INVALID_PARAMS, err.Error())
	}
	news, err := m.newsDAO.QueryById(req.Id)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	if news.Id == 0 {
		err = log.Errorf("news id [%d] not found", news.Id)
		return nil, itypes.NewBizCode(itypes.CODE_INVALID_PARAMS, err.Error())
	}
	news.Category = req.Category
	news.MainTitle = req.MainTitle
	news.SubTitle = req.SubTitle
	news.Summary = req.Summary
	news.Keywords = utils.JsonMarshal(req.Keywords)
	news.SeoKeywords = utils.JsonMarshal(req.SeoKeywords)
	news.Tags = req.Tags
	news.ImageUrl = req.ImageUrl
	news.Content = req.Content
	// news.ExtraData.Logs = models.MakeChangeLog(news.ExtraData.Logs, ctx.UserName(), models.OperType_Edit)

	var prevLogs []*models.ChangeLog
	if raw, ok := news.ExtraData["logs"]; ok {
		if arr, ok := raw.([]interface{}); ok {
			// 安全解析为 []*ChangeLog
			if bytes, err := json.Marshal(arr); err == nil {
				_ = json.Unmarshal(bytes, &prevLogs)
			}
		}
	}
	news.ExtraData["logs"] = models.MakeChangeLog(prevLogs, ctx.UserName(), models.OperType_Edit)

	_, err = m.newsDAO.Update(news,
		models.NEWS_COLUMN_CATEGORY,
		models.NEWS_COLUMN_MAIN_TITLE,
		models.NEWS_COLUMN_SUB_TITLE,
		models.NEWS_COLUMN_SUMMARY,
		models.NEWS_COLUMN_KEYWORDS,
		models.NEWS_COLUMN_SEO_KEYWORDS,
		models.NEWS_COLUMN_TAGS,
		models.NEWS_COLUMN_IMAGE_URL,
		models.NEWS_COLUMN_CONTENT,
		models.NEWS_COLUMN_EXTRA_DATA)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.SubEditNewsResp{}, itypes.BizOK
}

func (m *BizCore) TagList(ctx *itypes.Context, req *proto.TagListReq) (resp *proto.TagListResp, total int64, code itypes.BizCode) {
	var err error
	var dos []*models.TagDO
	dos, total, err = m.tagDAO.QueryAll(req.PageNo, req.PageSize, req.OrderAsc)
	if err != nil {
		return nil, 0, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.TagListResp{
		List: dos,
	}, total, itypes.BizOK
}

func (m *BizCore) TagAdd(ctx *itypes.Context, req *proto.TagAddReq) (resp *proto.TagAddResp, code itypes.BizCode) {
	dos, err := m.tagDAO.QueryByCondition(map[string]interface{}{
		models.TAG_COLUMN_NAME:       req.Name,
		models.TAG_COLUMN_NAME_CN:    req.NameCN,
		models.TAG_COLUMN_IS_DELETED: 0,
	})
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	if len(dos) != 0 {
		log.Errorf("tag name %s already exists", req.Name)
		return nil, itypes.NewBizCode(itypes.CODE_ALREADY_EXIST)
	}
	logs := models.MakeChangeLog(nil, ctx.UserName(), models.OperType_Edit)

	var id int64
	id, err = m.tagDAO.Insert(&models.TagDO{
		Name:       req.Name,
		IsInherent: false,
		IsDeleted:  false,
		NameCn:     req.NameCN,
		ExtraData: models.CommonExtraData{
			Logs: logs,
		},
	})
	return &proto.TagAddResp{
		Id: id,
	}, itypes.BizOK
}

func (m *BizCore) TagEdit(ctx *itypes.Context, req *proto.TagEditReq) (resp *proto.TagEditResp, code itypes.BizCode) {
	tag, err := m.tagDAO.QueryById(req.Id)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	if tag.Id == 0 {
		err = log.Errorf("tag id %s not exists", req.Id)
		return nil, itypes.NewBizCode(itypes.CODE_ALREADY_EXIST, err.Error())
	}
	if tag.IsInherent {
		return nil, itypes.NewBizCode(itypes.CODE_ACCESS_DENY, "no privilege to edit the inherent tag")
	}
	var columns = []string{
		models.TAG_COLUMN_NAME,
		models.TAG_COLUMN_EXTRA_DATA,
		models.TAG_COLUMN_NAME_CN,
	}
	tag.Name = req.Name
	tag.NameCn = req.NameCN
	tag.ExtraData.Logs = models.MakeChangeLog(tag.ExtraData.Logs, ctx.UserName(), models.OperType_Edit)

	_, err = m.tagDAO.Update(tag, columns...)
	if err != nil {
		return nil, itypes.NewBizCodeDatabaseError(err.Error())
	}
	return &proto.TagEditResp{}, itypes.BizOK
}

func (m *BizCore) TagDelete(ctx *itypes.Context, req *proto.TagDeleteReq) (resp *proto.TagDeleteResp, code itypes.BizCode) {
	for _, id := range req.Ids {
		tag, err := m.tagDAO.QueryById(id)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
		if tag.Id == 0 {
			err = log.Errorf("tag id %s not exists", id)
			return nil, itypes.NewBizCode(itypes.CODE_ALREADY_EXIST, err.Error())
		}
		if tag.IsInherent {
			return nil, itypes.NewBizCode(itypes.CODE_ACCESS_DENY, "no privilege to delete the inherent tag")
		}
		tag.IsDeleted = true
		tag.ExtraData.Logs = models.MakeChangeLog(tag.ExtraData.Logs, ctx.UserName(), models.OperType_Edit)

		_, err = m.tagDAO.Update(tag, models.TAG_COLUMN_IS_DELETED, models.TAG_COLUMN_EXTRA_DATA)
		if err != nil {
			return nil, itypes.NewBizCodeDatabaseError(err.Error())
		}
	}

	return &proto.TagDeleteResp{}, itypes.BizOK
}

func (m *BizCore) NewsAsync(ctx *itypes.Context, req *proto.NewsAsyncReq, needExtra bool) (resp *proto.NewsAsyncResp, total int64, code itypes.BizCode) {
	dos, total, err := m.newsDAO.QueryAsync(&dao.NewsAsyncCondition{
		Org_Id: req.Org_Id,
		Digest: req.Digest,
	})
	if err != nil {
		return nil, 0, itypes.NewBizCodeDatabaseError(err.Error())
	}

	return &proto.NewsAsyncResp{
		List: dos,
	}, total, itypes.BizOK
}

// NewsAsyncBatch：批量差异同步
func (m *BizCore) NewsAsyncBatch(ctx *itypes.Context, req *proto.NewsAsyncBatchReq, needExtra bool) (resp *proto.NewsAsyncResp, total int64, code itypes.BizCode) {
	const batchSize = 5000 // 防止 SQL 占位符过长，按需调整

	if len(req.List) == 0 {
		return &proto.NewsAsyncResp{List: []*models.NewsDO{}}, 0, itypes.BizOK
	}

	var all []*models.NewsDO

	for start := 0; start < len(req.List); start += batchSize {
		end := start + batchSize
		if end > len(req.List) {
			end = len(req.List)
		}

		// 把这一块转成 DAO 参数 ──
		pairs := make([]dao.OrgDigestPair, end-start)
		for i, v := range req.List[start:end] {
			pairs[i] = dao.OrgDigestPair{OrgId: v.Org_Id, Digest: v.Digest}
		}

		// 一次 SQL 抓差异 ──
		chunk, _, err := m.newsDAO.QueryAsyncBatch(pairs)
		if err != nil {
			return nil, 0, itypes.NewBizCodeDatabaseError(err.Error())
		}

		all = append(all, chunk...)
	}

	return &proto.NewsAsyncResp{
		List: all,
	}, int64(len(all)), itypes.BizOK
}

func (m *BizCore) NewsPullNew(ctx *itypes.Context, req *proto.NewsPullNewReq) (resp *proto.NewsPullNewResp, total int64, code itypes.BizCode) {
	// 1. 转换 List 为 Pairs（只用 OrgId，Digest 忽略但结构体字段需要赋值）
	pairs := make([]dao.OrgDigestPair, 0, len(req.List))
	for _, item := range req.List {
		pairs = append(pairs, dao.OrgDigestPair{
			OrgId:  item.OrgID, // 这个字段名必须匹配 dao.OrgDigestPair 里的定义
			Digest: "",         // 虽然不处理，但结构体字段得占上
		})
	}

	// 2. 调用 DAO 查询
	dos, total, err := m.newsDAO.QueryPullNew(&dao.NewsPullNewCondition{
		Pairs: pairs,
	})
	if err != nil {
		return nil, 0, itypes.NewBizCodeDatabaseError(err.Error())
	}

	// 3. 返回结果
	return &proto.NewsPullNewResp{
		List: dos,
	}, total, itypes.BizOK
}
