package system

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
	"kra/internal/biz"
	"strconv"
	"strings"
	"time"
)

type exportTemplatePO struct {
	ID           uint `gorm:"primaryKey"`
	CreatedAt    time.Time
	UpdatedAt    time.Time
	DeletedAt    gorm.DeletedAt `gorm:"index"`
	DBName       string
	Name         string
	DBTableName  string `gorm:"column:table_name"`
	TemplateID   string
	TemplateInfo string `gorm:"type:text"`
	SQL          string `gorm:"type:text"`
	ImportSQL    string `gorm:"type:text"`
	Limit        *int
	Order        string
}

func (exportTemplatePO) TableName() string { return "sys_export_templates" }

type exportConditionPO struct {
	ID         uint `gorm:"primaryKey"`
	CreatedAt  time.Time
	UpdatedAt  time.Time
	DeletedAt  gorm.DeletedAt `gorm:"index"`
	TemplateID string         `gorm:"index"`
	From       string
	Column     string
	Operator   string
}

func (exportConditionPO) TableName() string { return "sys_export_template_condition" }

type exportJoinPO struct {
	ID         uint `gorm:"primaryKey"`
	CreatedAt  time.Time
	UpdatedAt  time.Time
	DeletedAt  gorm.DeletedAt `gorm:"index"`
	TemplateID string         `gorm:"index"`
	Join       string         `gorm:"column:joins"`
	Table      string
	On         string `gorm:"column:on"`
}

func (exportJoinPO) TableName() string { return "sys_export_template_join" }

type exportRepo struct{ data Provider }

func NewExportRepo(data Provider) biz.ExportRepo { return &exportRepo{data: data} }
func exportFromPO(po exportTemplatePO, conditions []exportConditionPO, joins []exportJoinPO) *biz.ExportTemplate {
	v := &biz.ExportTemplate{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DBName: po.DBName, Name: po.Name, TableName: po.DBTableName, TemplateID: po.TemplateID, TemplateInfo: po.TemplateInfo, SQL: po.SQL, ImportSQL: po.ImportSQL, Limit: po.Limit, Order: po.Order}
	if conditions != nil {
		v.Conditions = make([]biz.ExportCondition, 0, len(conditions))
	}
	for _, x := range conditions {
		v.Conditions = append(v.Conditions, biz.ExportCondition{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, From: x.From, Column: x.Column, Operator: x.Operator})
	}
	if joins != nil {
		v.Joins = make([]biz.ExportJoin, 0, len(joins))
	}
	for _, x := range joins {
		v.Joins = append(v.Joins, biz.ExportJoin{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, Join: x.Join, Table: x.Table, On: x.On})
	}
	return v
}
func exportToPO(v *biz.ExportTemplate) exportTemplatePO {
	return exportTemplatePO{ID: v.ID, DBName: v.DBName, Name: v.Name, DBTableName: v.TableName, TemplateID: v.TemplateID, TemplateInfo: v.TemplateInfo, SQL: v.SQL, ImportSQL: v.ImportSQL, Limit: v.Limit, Order: v.Order}
}
func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate, resetIDs, replace, forceTemplateID bool) error {
	if replace {
		if err := tx.Where("template_id = ?", v.TemplateID).Delete(&exportConditionPO{}).Error; err != nil {
			return err
		}
		if err := tx.Where("template_id = ?", v.TemplateID).Delete(&exportJoinPO{}).Error; err != nil {
			return err
		}
	}
	conditions := make([]exportConditionPO, 0, len(v.Conditions))
	for _, x := range v.Conditions {
		id := x.ID
		if resetIDs {
			id = 0
		}
		templateID := x.TemplateID
		// GORM's has-many association on Create always writes the parent's
		// TemplateID into the child foreign key (references:TemplateID).
		// Preserve that observable behavior for creates; updates intentionally
		// keep the request's value after the compatible update deletes and
		// recreates relations.
		if forceTemplateID || templateID == "" {
			templateID = v.TemplateID
		}
		conditions = append(conditions, exportConditionPO{ID: id, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: templateID, From: x.From, Column: x.Column, Operator: x.Operator})
	}
	if len(conditions) > 0 {
		create := tx
		if !resetIDs {
			// GORM saves Create associations with ON CONFLICT DO NOTHING.
			// Preserve that behavior for copied templates carrying relation IDs.
			create = create.Clauses(clause.OnConflict{DoNothing: true})
		}
		if err := create.Create(&conditions).Error; err != nil {
			return err
		}
	}
	joins := make([]exportJoinPO, 0, len(v.Joins))
	for _, x := range v.Joins {
		id := x.ID
		if resetIDs {
			id = 0
		}
		templateID := x.TemplateID
		if forceTemplateID || templateID == "" {
			templateID = v.TemplateID
		}
		joins = append(joins, exportJoinPO{ID: id, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: templateID, Join: x.Join, Table: x.Table, On: x.On})
	}
	if len(joins) > 0 {
		create := tx
		if !resetIDs {
			create = create.Clauses(clause.OnConflict{DoNothing: true})
		}
		return create.Create(&joins).Error
	}
	return nil
}
func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error {
	return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		po := exportToPO(v)
		if err := tx.Create(&po).Error; err != nil {
			return err
		}
		v.ID = po.ID
		return r.saveRelations(tx, v, false, false, true)
	})
}
func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error {
	return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		po := exportToPO(v)
		if err := tx.Model(&exportTemplatePO{}).Where("id = ?", v.ID).Updates(&po).Error; err != nil {
			return err
		}
		return r.saveRelations(tx, v, true, true, false)
	})
}
func (r *exportRepo) DeleteExportTemplates(ctx context.Context, ids []uint) error {
	return r.data.DB().WithContext(ctx).Delete(&[]exportTemplatePO{}, "id IN ?", ids).Error
}
func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string) (*biz.ExportTemplate, error) {
	var po exportTemplatePO
	db := r.data.DB().WithContext(ctx)
	var err error
	if tid != "" {
		err = db.Where("template_id = ?", tid).First(&po).Error
	} else {
		err = db.Where("id = ?", id).First(&po).Error
	}
	if err != nil {
		return nil, err
	}
	var conditions []exportConditionPO
	var joins []exportJoinPO
	if err = db.Where("template_id = ?", po.TemplateID).Find(&conditions).Error; err != nil {
		return nil, err
	}
	if err = db.Where("template_id = ?", po.TemplateID).Find(&joins).Error; err != nil {
		return nil, err
	}
	return exportFromPO(po, conditions, joins), nil
}
func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q *biz.ExportTemplate) ([]*biz.ExportTemplate, int64, error) {
	db := r.data.DB().WithContext(ctx).Model(&exportTemplatePO{})
	if q != nil {
		if q.StartCreatedAt != nil && q.EndCreatedAt != nil {
			db = db.Where("created_at BETWEEN ? AND ?", q.StartCreatedAt, q.EndCreatedAt)
		}
		if q.Name != "" {
			db = db.Where("name LIKE ?", "%"+q.Name+"%")
		}
		if q.TableName != "" {
			db = db.Where("table_name = ?", q.TableName)
		}
		if q.TemplateID != "" {
			db = db.Where("template_id = ?", q.TemplateID)
		}
	}
	var total int64
	if err := db.Count(&total).Error; err != nil {
		return nil, 0, err
	}
	var pos []exportTemplatePO
	// This module applies pageSize and its raw offset formula verbatim instead
	// of using the shared PageInfo normalization.
	if size != 0 {
		db = db.Limit(size).Offset(size * (page - 1))
	}
	if err := db.Find(&pos).Error; err != nil {
		return nil, 0, err
	}
	out := make([]*biz.ExportTemplate, 0, len(pos))
	for _, po := range pos {
		out = append(out, exportFromPO(po, nil, nil))
	}
	return out, total, nil
}

func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, params map[string]string) ([]map[string]any, string, error) {
	selected, err := r.data.Database(t.DBName)
	if err != nil {
		return nil, "", err
	}
	db := selected.WithContext(ctx)
	var rows []map[string]any
	if t.SQL != "" {
		args := map[string]any{}
		for k, v := range params {
			args[k] = v
		}
		err := db.Raw(t.SQL, args).Scan(&rows).Error
		return rows, t.SQL, err
	}
	columns := make([]string, 0)
	if err := json.Unmarshal([]byte(t.TemplateInfo), &map[string]string{}); err != nil {
		return nil, "", err
	}
	decoder := json.NewDecoder(strings.NewReader(t.TemplateInfo))
	_, _ = decoder.Token()
	for decoder.More() {
		key, _ := decoder.Token()
		columns = append(columns, key.(string))
		var ignored any
		_ = decoder.Decode(&ignored)
	}
	if len(columns) == 0 {
		return nil, "", errors.New("模板列为空")
	}
	query := db.Table(t.TableName).Select(strings.Join(columns, ","))
	for _, j := range t.Joins {
		query = query.Joins(j.Join + " " + j.Table + " ON " + j.On)
	}
	if params["filterDeleted"] == "true" {
		query = query.Where(fmt.Sprintf("%s.deleted_at IS NULL", t.TableName))
		for _, join := range t.Joins {
			if db.Migrator().HasColumn(join.Table, "deleted_at") {
				query = query.Where(fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
			}
		}
	}
	for _, condition := range t.Conditions {
		operator := condition.Operator
		value := params[condition.From]
		switch operator {
		case "LIKE":
			if value != "" {
				query = query.Where(condition.Column+" LIKE ?", "%"+value+"%")
			}
		case "IN", "NOT IN":
			if value != "" {
				query = query.Where(condition.Column+" "+operator+" (?)", value)
			}
		case "BETWEEN":
			start, end := params["start"+condition.From], params["end"+condition.From]
			if start != "" && end != "" {
				query = query.Where(condition.Column+" BETWEEN ? AND ?", start, end)
			}
		default:
			if value != "" {
				query = query.Where(condition.Column+" "+operator+" ?", value)
			}
		}
	}
	limit := 0
	if raw := params["limit"]; raw != "" {
		limit, _ = strconv.Atoi(raw)
	} else if t.Limit != nil {
		limit = *t.Limit
	}
	if limit > 0 {
		query = query.Limit(limit)
	}
	if raw := params["offset"]; raw != "" {
		if offset, parseErr := strconv.Atoi(raw); parseErr == nil {
			query = query.Offset(offset)
		}
	}
	order := params["order"]
	if order == "" {
		order = t.Order
	}
	if order != "" {
		parts := strings.Split(order, " ")
		fields := map[string]bool{}
		columnTypes, columnErr := db.Migrator().ColumnTypes(t.TableName)
		if columnErr != nil {
			return nil, "", columnErr
		}
		for _, column := range columnTypes {
			fields[column.Name()] = true
		}
		if !fields[parts[0]] {
			return nil, "", fmt.Errorf("order by %s is not in the fields", order)
		}
		orderSQL := parts[0]
		if len(parts) > 1 {
			if parts[1] != "asc" && parts[1] != "desc" {
				return nil, "", fmt.Errorf("order by %s is not secure", order)
			}
			orderSQL += " " + parts[1]
		}
		query = query.Order(orderSQL)
	}
	preview := query.ToSQL(func(tx *gorm.DB) *gorm.DB { return tx.Find(&[]map[string]any{}) })
	err = query.Find(&rows).Error
	return rows, preview, err
}

func (r *exportRepo) PreviewExport(ctx context.Context, t *biz.ExportTemplate, params map[string]string) (string, error) {
	columns := make([]string, 0)
	decoder := json.NewDecoder(strings.NewReader(t.TemplateInfo))
	if token, err := decoder.Token(); err != nil || token != json.Delim('{') {
		return "", err
	}
	for decoder.More() {
		key, err := decoder.Token()
		if err != nil {
			return "", err
		}
		columns = append(columns, key.(string))
		var ignored any
		if err = decoder.Decode(&ignored); err != nil {
			return "", err
		}
	}
	var builder strings.Builder
	builder.WriteString("SELECT ")
	builder.WriteString(strings.Join(columns, ", "))
	builder.WriteString(" FROM ")
	builder.WriteString(t.TableName)
	for _, join := range t.Joins {
		builder.WriteString(" " + join.Join + " " + join.Table + " ON " + join.On)
	}
	wheres := make([]string, 0)
	if params["filterDeleted"] == "true" {
		wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", t.TableName))
		selected, err := r.data.Database(t.DBName)
		if err != nil {
			return "", err
		}
		db := selected.WithContext(ctx)
		for _, join := range t.Joins {
			if db.Migrator().HasColumn(join.Table, "deleted_at") {
				wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
			}
		}
	}
	for _, condition := range t.Conditions {
		op, column, value := strings.ToUpper(strings.TrimSpace(condition.Operator)), strings.TrimSpace(condition.Column), params[condition.From]
		switch op {
		case "BETWEEN":
			start, end := params["start"+condition.From], params["end"+condition.From]
			if start != "" && end != "" {
				wheres = append(wheres, fmt.Sprintf("%s BETWEEN '%s' AND '%s'", column, start, end))
			} else {
				wheres = append(wheres, fmt.Sprintf("%s BETWEEN {start%s} AND {end%s}", column, condition.From, condition.From))
			}
		case "IN", "NOT IN":
			if value != "" {
				parts := strings.Split(value, ",")
				for index := range parts {
					parts[index] = strings.TrimSpace(parts[index])
				}
				wheres = append(wheres, fmt.Sprintf("%s %s ('%s')", column, op, strings.Join(parts, "','")))
			} else {
				wheres = append(wheres, fmt.Sprintf("%s %s ({%s})", column, op, condition.From))
			}
		case "LIKE":
			if value != "" {
				wheres = append(wheres, fmt.Sprintf("%s LIKE '%%%s%%'", column, value))
			} else {
				wheres = append(wheres, fmt.Sprintf("%s LIKE {%%%s%%}", column, condition.From))
			}
		default:
			if value != "" {
				wheres = append(wheres, fmt.Sprintf("%s %s '%s'", column, op, value))
			} else {
				wheres = append(wheres, fmt.Sprintf("%s %s {%s}", column, op, condition.From))
			}
		}
	}
	if len(wheres) > 0 {
		builder.WriteString(" WHERE " + strings.Join(wheres, " AND "))
	}
	order := params["order"]
	if order == "" {
		order = t.Order
	}
	if order != "" {
		builder.WriteString(" ORDER BY " + order)
	}
	limitRaw, offsetRaw := params["limit"], params["offset"]
	if limitRaw == "" && t.Limit != nil && *t.Limit != 0 {
		limitRaw = strconv.Itoa(*t.Limit)
	}
	limit, _ := strconv.Atoi(limitRaw)
	offset, _ := strconv.Atoi(offsetRaw)
	if limit > 0 {
		builder.WriteString(" LIMIT " + strconv.Itoa(limit))
		if offset > 0 {
			builder.WriteString(" OFFSET " + strconv.Itoa(offset))
		}
	} else if offset > 0 {
		builder.WriteString(" OFFSET " + strconv.Itoa(offset))
	}
	return builder.String(), nil
}
func parseTemplateColumns(raw string) map[string]string {
	out := map[string]string{}
	parts := strings.Split(strings.Trim(strings.TrimSpace(raw), "{}"), ",")
	for _, part := range parts {
		pair := strings.SplitN(part, ":", 2)
		if len(pair) == 2 {
			out[strings.Trim(strings.TrimSpace(pair[0]), `"`)] = strings.Trim(strings.TrimSpace(pair[1]), `"`)
		}
	}
	return out
}
func (r *exportRepo) ImportExportRows(ctx context.Context, t *biz.ExportTemplate, rows []map[string]any) error {
	// ImportSQL is checked verbatim. In particular, a whitespace-only value
	// is still treated as custom SQL and is allowed to return the driver's
	// native error instead of silently falling back to GORM insertion.
	sql := t.ImportSQL
	selected, err := r.data.Database(t.DBName)
	if err != nil {
		return err
	}
	return selected.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if sql != "" {
			for _, row := range rows {
				if err := tx.Exec(sql, row).Error; err != nil {
					return err
				}
			}
			return nil
		}
		needCreated := tx.Migrator().HasColumn(t.TableName, "created_at")
		needUpdated := tx.Migrator().HasColumn(t.TableName, "updated_at")
		for _, row := range rows {
			if row["created_at"] == nil && needCreated {
				row["created_at"] = time.Now()
			}
			if row["updated_at"] == nil && needUpdated {
				row["updated_at"] = time.Now()
			}
		}
		return tx.Table(t.TableName).CreateInBatches(&rows, 1000).Error
	})
}
