package usecases

import (
	"cargo-erp-backend/internal/domain"
	"cargo-erp-backend/internal/handlers/dto"
	"cargo-erp-backend/pkg/datatable"
	"cargo-erp-backend/pkg/helpers"
	"encoding/json"
	"fmt"
	"strings"
	"sync"
	"time"

	"go.uber.org/zap"
	"gorm.io/gorm"
)

type CascadeRule struct {
	Table      string
	FKColumn   string
	DeleteRule string
}

type CascadeGroup struct {
	Table      string                   `json:"table"`
	FKColumn   string                   `json:"fk_column"`
	DeleteRule string                   `json:"delete_rule"`
	Records    []map[string]interface{} `json:"records"`
}

var (
	CascadeMap   map[string][]CascadeRule
	CascadeMapMu sync.RWMutex
)

type ActivityLogUsecaseInterface interface {
	GetAll(filter dto.ActivityLogFilterRequest) ([]dto.ActivityLogResponse, int64, error)
	GetList(req dto.DataTableRequest) (dto.DataTableResponse, error)
	GetByID(id string) (dto.ActivityLogResponse, error)
	Revert(logID string) error
	LogCreate(tableName string, record interface{}, merchantID *string)
	LogUpdate(tableName string, oldData, newData interface{}, merchantID *string)
	LogDelete(tableName string, recordID string, oldData interface{}, merchantID *string)
}

type ActivityLogUsecase struct {
	Helper   helpers.HelperInterface
	AuthUser domain.User
}

func NewActivityLogUsecase(helper helpers.HelperInterface, authUser domain.User) ActivityLogUsecaseInterface {
	return &ActivityLogUsecase{Helper: helper, AuthUser: authUser}
}

func LoadCascadeRules(db *gorm.DB) error {
	rows, err := db.Raw(`
		SELECT 
			tc.table_name AS source_table,
			kcu.column_name AS source_column,
			ccu.table_name AS target_table,
			ccu.column_name AS target_column,
			rc.delete_rule
		FROM information_schema.table_constraints tc
		JOIN information_schema.key_column_usage kcu 
			ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
		JOIN information_schema.constraint_column_usage ccu 
			ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = ccu.table_schema
		JOIN information_schema.referential_constraints rc 
			ON rc.constraint_name = tc.constraint_name AND rc.constraint_schema = tc.table_schema
		WHERE tc.constraint_type = 'FOREIGN KEY'
		  AND tc.table_schema = 'public'
		ORDER BY tc.table_name
	`).Rows()
	if err != nil {
		return fmt.Errorf("failed to query FK constraints: %w", err)
	}
	defer rows.Close()

	newMap := make(map[string][]CascadeRule)
	for rows.Next() {
		var fk ForeignKeyInfo
		if err := rows.Scan(&fk.SourceTable, &fk.SourceColumn, &fk.TargetTable, &fk.TargetColumn, &fk.DeleteRule); err != nil {
			continue
		}
		if _, ok := newMap[fk.TargetTable]; !ok {
			newMap[fk.TargetTable] = make([]CascadeRule, 0)
		}
		newMap[fk.TargetTable] = append(newMap[fk.TargetTable], CascadeRule{
			Table:      fk.SourceTable,
			FKColumn:   fk.SourceColumn,
			DeleteRule: fk.DeleteRule,
		})
	}

	CascadeMapMu.Lock()
	CascadeMap = newMap
	CascadeMapMu.Unlock()
	return nil
}

type ForeignKeyInfo struct {
	SourceTable   string
	SourceColumn  string
	TargetTable   string
	TargetColumn  string
	DeleteRule    string
}

func CaptureCascade(tx *gorm.DB, tableName string, recordID string) ([]CascadeGroup, error) {
	groups := make([]CascadeGroup, 0)
	visited := make(map[string]bool)

	CascadeMapMu.RLock()
	defer CascadeMapMu.RUnlock()

	var walk func(table string, ids []string)
	walk = func(table string, ids []string) {
		if len(ids) == 0 {
			return
		}
		rules, ok := CascadeMap[table]
		if !ok {
			return
		}
		for _, rule := range rules {
			key := rule.Table + ":" + strings.Join(ids, ",")
			if visited[key] {
				continue
			}
			visited[key] = true

			var records []map[string]interface{}
			if err := tx.Table(rule.Table).Where(rule.FKColumn+" IN ?", ids).Find(&records).Error; err != nil {
				continue
			}
			if len(records) > 0 {
				groups = append(groups, CascadeGroup{
					Table:      rule.Table,
					FKColumn:   rule.FKColumn,
					DeleteRule: rule.DeleteRule,
					Records:    records,
				})
				childIDs := extractIDs(records, "id")
				walk(rule.Table, childIDs)
			}
		}
	}

	walk(tableName, []string{recordID})
	return groups, nil
}

func extractIDs(records []map[string]interface{}, key string) []string {
	ids := make([]string, 0, len(records))
	for _, r := range records {
		if v, ok := r[key]; ok && v != nil {
			switch id := v.(type) {
			case string:
				ids = append(ids, id)
			case float64:
				ids = append(ids, fmt.Sprintf("%.0f", id))
			}
		}
	}
	return ids
}

func toJSON(v interface{}) *string {
	if v == nil {
		return nil
	}
	b, err := json.Marshal(v)
	if err != nil {
		return nil
	}
	s := string(b)
	return &s
}

func (u *ActivityLogUsecase) LogCreate(tableName string, record interface{}, merchantID *string) {
	db := u.Helper.GetDB("master")
	log := domain.ActivityLog{
		TargetTable: tableName,
		RecordID:    extractRecordID(record),
		Action:      "CREATE",
		NewData:     toJSON(record),
		UserID:      u.AuthUser.ID,
		UserName:    u.getDisplayName(),
		MerchantID:  merchantID,
		CreatedOn:   timePtr(time.Now()),
	}
	db.Create(&log)
}

func (u *ActivityLogUsecase) LogUpdate(tableName string, oldData, newData interface{}, merchantID *string) {
	db := u.Helper.GetDB("master")
	log := domain.ActivityLog{
		TargetTable: tableName,
		RecordID:    extractRecordID(oldData),
		Action:      "UPDATE",
		OldData:     toJSON(oldData),
		NewData:     toJSON(newData),
		UserID:      u.AuthUser.ID,
		UserName:    u.getDisplayName(),
		MerchantID:  merchantID,
		CreatedOn:   timePtr(time.Now()),
	}
	db.Create(&log)
}

func (u *ActivityLogUsecase) LogDelete(tableName string, recordID string, oldData interface{}, merchantID *string) {
	db := u.Helper.GetDB("master")

	var cascadeData []CascadeGroup
	err := db.Transaction(func(tx *gorm.DB) error {
		var err error
		cascadeData, err = CaptureCascade(tx, tableName, recordID)
		return err
	})
	if err != nil {
		u.Helper.Log().Error("failed to capture cascade", zap.Error(err))
		cascadeData = []CascadeGroup{}
	}

	log := domain.ActivityLog{
		TargetTable: tableName,
		RecordID:    recordID,
		Action:      "DELETE",
		OldData:     toJSON(oldData),
		CascadeData: toJSON(cascadeData),
		UserID:      u.AuthUser.ID,
		UserName:    u.getDisplayName(),
		MerchantID:  merchantID,
		CreatedOn:   timePtr(time.Now()),
	}
	db.Create(&log)
}

func (u *ActivityLogUsecase) GetAll(filter dto.ActivityLogFilterRequest) ([]dto.ActivityLogResponse, int64, error) {
	var logs []domain.ActivityLog
	var total int64

	db := u.Helper.GetDB("slave")
	tx := db.Model(&logs)

	if filter.TableName != "" {
		tx = tx.Where("table_name = ?", filter.TableName)
	}
	if filter.RecordID != "" {
		tx = tx.Where("record_id = ?", filter.RecordID)
	}
	if filter.Action != "" {
		tx = tx.Where("action = ?", filter.Action)
	}
	if filter.UserID != "" {
		tx = tx.Where("user_id = ?", filter.UserID)
	}
	if filter.MerchantID != "" {
		tx = tx.Where("merchant_id = ?", filter.MerchantID)
	}
	if filter.DateFrom != "" {
		tx = tx.Where("created_on >= ?", filter.DateFrom)
	}
	if filter.DateTo != "" {
		tx = tx.Where("created_on <= ?", filter.DateTo)
	}

	tx.Count(&total)
	tx.Order("created_on DESC")
	if err := tx.Find(&logs).Error; err != nil {
		return nil, 0, err
	}

	responses := make([]dto.ActivityLogResponse, 0, len(logs))
	for _, l := range logs {
		responses = append(responses, toActivityLogResponse(l))
	}
	return responses, total, nil
}

func (u *ActivityLogUsecase) GetList(req dto.DataTableRequest) (dto.DataTableResponse, error) {
	db := u.Helper.GetDB("slave")
	tx := db.Table("activity_log")
	dt := datatable.NewDatatable(tx, req)
	coldef := make([]dto.DataTableColDef, 0)
	coldef = append(coldef, dto.DataTableColDef{Field: "activity_log.table_name", Alias: "table_name"})
	coldef = append(coldef, dto.DataTableColDef{Field: "activity_log.record_id", Alias: "record_id"})
	coldef = append(coldef, dto.DataTableColDef{Field: "activity_log.action", Alias: "action"})
	coldef = append(coldef, dto.DataTableColDef{Field: "activity_log.user_id", Alias: "user_id"})
	coldef = append(coldef, dto.DataTableColDef{Field: "activity_log.user_name", Alias: "user_name"})
	coldef = append(coldef, dto.DataTableColDef{Field: "activity_log.id", Alias: "id"})
	return dt.Render(coldef), nil
}

func (u *ActivityLogUsecase) GetByID(id string) (dto.ActivityLogResponse, error) {
	var log domain.ActivityLog
	db := u.Helper.GetDB("slave")
	if err := db.Where("id = ?", id).First(&log).Error; err != nil {
		return dto.ActivityLogResponse{}, err
	}
	return toActivityLogResponse(log), nil
}

func (u *ActivityLogUsecase) Revert(logID string) error {
	db := u.Helper.GetDB("master")

	var log domain.ActivityLog
	if err := db.Where("id = ?", logID).First(&log).Error; err != nil {
		return fmt.Errorf("activity log not found")
	}

	switch log.Action {
	case "CREATE":
		return u.revertCreate(db, log)
	case "UPDATE":
		return u.revertUpdate(db, log)
	case "DELETE":
		return u.revertDelete(db, log)
	default:
		return fmt.Errorf("unsupported action: %s", log.Action)
	}
}

func (u *ActivityLogUsecase) revertCreate(db *gorm.DB, log domain.ActivityLog) error {
	return db.Transaction(func(tx *gorm.DB) error {
		if err := tx.Exec("DELETE FROM "+log.TargetTable+" WHERE id = ?", log.RecordID).Error; err != nil {
			return err
		}
		u.logRevert("CREATE", log)
		return nil
	})
}

func (u *ActivityLogUsecase) revertUpdate(db *gorm.DB, log domain.ActivityLog) error {
	return db.Transaction(func(tx *gorm.DB) error {
		if log.OldData == nil {
			return fmt.Errorf("no old data to revert")
		}

		var oldRecord map[string]interface{}
		if err := json.Unmarshal([]byte(*log.OldData), &oldRecord); err != nil {
			return fmt.Errorf("failed to parse old data: %w", err)
		}

		cleaned := cleanRecordMap(oldRecord)
		delete(cleaned, "created_on")
		delete(cleaned, "created_by")

		if err := tx.Table(log.TargetTable).Where("id = ?", log.RecordID).Updates(cleaned).Error; err != nil {
			return err
		}

		u.logRevert("UPDATE", log)
		return nil
	})
}

func (u *ActivityLogUsecase) revertDelete(db *gorm.DB, log domain.ActivityLog) error {
	return db.Transaction(func(tx *gorm.DB) error {
		var cascadeGroups []CascadeGroup
		if log.CascadeData != nil {
			if err := json.Unmarshal([]byte(*log.CascadeData), &cascadeGroups); err != nil {
				return fmt.Errorf("failed to parse cascade data: %w", err)
			}
		}

		for i := len(cascadeGroups) - 1; i >= 0; i-- {
			group := cascadeGroups[i]
			for _, record := range group.Records {
				cleaned := cleanRecordMap(record)
				tx.Table(group.Table).Create(cleaned)
			}
		}

		if log.OldData != nil {
			var mainRecord map[string]interface{}
			if err := json.Unmarshal([]byte(*log.OldData), &mainRecord); err == nil {
				cleaned := cleanRecordMap(mainRecord)
				tx.Table(log.TargetTable).Create(cleaned)
			}
		}

		u.logRevert("DELETE", log)
		return nil
	})
}

func (u *ActivityLogUsecase) logRevert(originalAction string, originalLog domain.ActivityLog) {
	db := u.Helper.GetDB("master")
	revertLog := domain.ActivityLog{
		TargetTable: originalLog.TargetTable,
		RecordID:   originalLog.RecordID,
		Action:     "REVERT",
		OldData:    toJSON(fmt.Sprintf("Reverted %s action (log_id: %s)", originalAction, originalLog.ID)),
		UserID:     u.AuthUser.ID,
		UserName:   u.getDisplayName(),
		MerchantID: originalLog.MerchantID,
		CreatedOn:  timePtr(time.Now()),
	}
	db.Create(&revertLog)
}

func (u *ActivityLogUsecase) getDisplayName() string {
	if u.AuthUser.NickName != nil && *u.AuthUser.NickName != "" {
		return *u.AuthUser.NickName
	}
	if u.AuthUser.Email != nil {
		return *u.AuthUser.Email
	}
	return u.AuthUser.ID
}

func extractRecordID(record interface{}) string {
	switch v := record.(type) {
	case map[string]interface{}:
		if id, ok := v["id"].(string); ok {
			return id
		}
	case domain.Province:
		return v.ID
	case domain.City:
		return v.ID
	case domain.District:
		return v.ID
	case domain.Subdistrict:
		return v.ID
	case domain.PostalCode:
		return v.ID
	case domain.CostComponent:
		return v.ID
	case domain.PackageType:
		return v.ID
	case domain.Moda:
		return v.ID
	case domain.BasicPricing:
		return v.ID
	}

	b, err := json.Marshal(record)
	if err != nil {
		return ""
	}
	var m map[string]interface{}
	if err := json.Unmarshal(b, &m); err != nil {
		return ""
	}
	if id, ok := m["id"].(string); ok {
		return id
	}
	return ""
}

func timePtr(t time.Time) *time.Time {
	return &t
}

func toActivityLogResponse(log domain.ActivityLog) dto.ActivityLogResponse {
	resp := dto.ActivityLogResponse{
		ID:          log.ID,
		TableName:   log.TargetTable,
		RecordID:    log.RecordID,
		Action:      log.Action,
		OldData:     log.OldData,
		NewData:     log.NewData,
		CascadeData: log.CascadeData,
		UserID:      log.UserID,
		UserName:    log.UserName,
		MerchantID:  log.MerchantID,
	}
	if log.CreatedOn != nil {
		s := log.CreatedOn.Format("2006-01-02 15:04:05")
		resp.CreatedOn = &s
	}
	if log.CascadeData != nil {
		var groups []CascadeGroup
		if err := json.Unmarshal([]byte(*log.CascadeData), &groups); err == nil {
			count := 0
			for _, g := range groups {
				count += len(g.Records)
			}
			resp.CascadeCount = count
		}
	}
	return resp
}

func cleanRecordMap(record map[string]interface{}) map[string]interface{} {
	cleaned := make(map[string]interface{})
	for k, v := range record {
		if v == nil {
			continue
		}
		switch v.(type) {
		case map[string]interface{}, []interface{}:
			continue
		}
		cleaned[k] = v
	}
	return cleaned
}
