package dbsync

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"time"
)

// PushItem 本机 agent → 线上 A 的一条变更（形态 B）。
type PushItem struct {
	Table          string           `json:"table"`
	Op             string           `json:"op"` // insert|update|update_by_id|delete|upsert
	RowPK          string           `json:"row_pk"`
	Row            map[string]any   `json:"row"`  // 单行；与 Rows 二选一
	Rows           []map[string]any `json:"rows"` // 兼容批量 insert
	Version        int64            `json:"version"` // outbox 单调 version；0 则用时间戳
	ClientOutboxID string           `json:"client_outbox_id"`
	OnlineDBID     string           `json:"online_db_id"`
}

// PushResult 单条推送结果。
type PushResult struct {
	OK             bool   `json:"ok"`
	Applied        bool   `json:"applied"`
	Skipped        bool   `json:"skipped"`
	Conflict       bool   `json:"conflict"`
	AppliedVersion int64  `json:"applied_version"`
	Message        string `json:"message"`
	ClientOutboxID string `json:"client_outbox_id,omitempty"`
}

// PushToRemote 仅打开 remote，按 LWW/幂等将变更落到线上 A。不连本机 SQLite。
func PushToRemote(ctx context.Context, ch *Channel, store *FileStore, item PushItem) (*PushResult, error) {
	if ch == nil {
		return nil, fmt.Errorf("channel is nil")
	}
	table := strings.TrimSpace(item.Table)
	if table == "" {
		return nil, fmt.Errorf("table required")
	}
	// Z4 冻结：通道表白名单不再作为拒收依据；Binding + JWT 才是权限源。
	pkCol := "id"
	if ch.PKColumns != nil && strings.TrimSpace(ch.PKColumns[table]) != "" {
		pkCol = ch.PKColumns[table]
	}

	op, payload, rowPK, err := normalizePushPayload(item, pkCol)
	if err != nil {
		return nil, err
	}
	version := item.Version
	if version <= 0 {
		version = time.Now().UnixNano()
	}

	db, err := AcquireRemote(ch.Remote.Driver, ch.Remote.DSN)
	if err != nil {
		return nil, wrapOpenRemote(err)
	}

	if err := EnsureMeta(ctx, db, ch.Remote.Driver); err != nil {
		return nil, Retryablef("ensure meta: %v", err)
	}

	if op != "delete" {
		var row map[string]any
		_ = json.Unmarshal([]byte(payload), &row)
		if err := EnsureTableFromRow(ctx, db, ch.Remote.Driver, table, pkCol, row); err != nil {
			return nil, Retryablef("ensure table: %v", err)
		}
	}

	res := &PushResult{
		ClientOutboxID: item.ClientOutboxID,
		AppliedVersion: version,
	}

	tgtVer, has, err := GetMetaVersion(ctx, db, ch.Remote.Driver, table, rowPK)
	if err != nil {
		return nil, Retryablef("meta version: %v", err)
	}
	// 幂等：同 version 已落地 → 跳过
	if has && tgtVer == version {
		res.OK = true
		res.Skipped = true
		res.Message = "already applied (same version)"
		if store != nil {
			_ = store.PatchStats(ch.ID, func(c *Channel) { c.Stats.PushedSkipped++ })
		}
		return res, nil
	}
	if has && tgtVer > version {
		policy := ch.ConflictPolicy
		if policy == "" {
			policy = PolicyLWWSource // B→A 默认偏源
		}
		switch policy {
		case PolicyLWWTarget:
			loser := payload
			winner := SnapshotTargetRow(ctx, db, ch.Remote.Driver, table, pkCol, rowPK)
			RecordLwwOverride(store, LwwOverride{
				TenantID:      ch.TenantID,
				ChannelID:     ch.ID,
				Table:         table,
				RowPK:         rowPK,
				Op:            op,
				Entry:         EntryAgentPush,
				Policy:        string(PolicyLWWTarget),
				Outcome:       OutcomeKept,
				LoserPayload:  loser,
				WinnerPayload: winner,
				TargetVer:     tgtVer,
				SourceVer:     version,
			})
			res.OK = true
			res.Skipped = true
			res.Message = "target newer; kept (lww_target)"
			if store != nil {
				_ = store.PatchStats(ch.ID, func(c *Channel) { c.Stats.PushedSkipped++ })
			}
			return res, nil
		case PolicyLWWSource:
			loser := SnapshotTargetRow(ctx, db, ch.Remote.Driver, table, pkCol, rowPK)
			if err := ApplyChange(ctx, db, ch.Remote.Driver, table, pkCol, op, payload, version); err != nil {
				return nil, Retryablef("apply: %v", err)
			}
			RecordLwwOverride(store, LwwOverride{
				TenantID:      ch.TenantID,
				ChannelID:     ch.ID,
				Table:         table,
				RowPK:         rowPK,
				Op:            op,
				Entry:         EntryAgentPush,
				Policy:        string(PolicyLWWSource),
				Outcome:       OutcomeApplied,
				LoserPayload:  loser,
				WinnerPayload: payload,
				TargetVer:     tgtVer,
				SourceVer:     version,
			})
			if store != nil {
				_ = store.PatchStats(ch.ID, func(c *Channel) {
					c.Stats.PushedOK++
					c.Stats.PushedApplied++
				})
			}
			res.OK = true
			res.Applied = true
			res.Message = "applied (lww_source override)"
			return res, nil
		default:
			if store != nil {
				_ = store.AddConflict(Conflict{
					TenantID:  ch.TenantID,
					ChannelID: ch.ID,
					Table:     table,
					RowPK:     rowPK,
					Op:        op,
					Source:    "agent",
					Payload:   payload,
					TargetVer: tgtVer,
					SourceVer: version,
					Message:   "target version newer than agent push",
				})
				_ = store.PatchStats(ch.ID, func(c *Channel) {
					c.Stats.Conflicts++
					c.Stats.PushedSkipped++
				})
			}
			res.OK = true
			res.Skipped = true
			res.Conflict = true
			res.Message = "queued conflict; target newer"
			return res, nil
		}
	}

	if err := ApplyChange(ctx, db, ch.Remote.Driver, table, pkCol, op, payload, version); err != nil {
		return nil, Retryablef("apply: %v", err)
	}
	if store != nil {
		_ = store.PatchStats(ch.ID, func(c *Channel) {
			c.Stats.PushedOK++
			c.Stats.PushedApplied++
		})
	}
	res.OK = true
	res.Applied = true
	res.Message = "applied"
	return res, nil
}

// PushBatchToRemote 保序批量；遇错即停，已成功条数在返回切片中。
func PushBatchToRemote(ctx context.Context, ch *Channel, store *FileStore, items []PushItem) ([]PushResult, error) {
	out := make([]PushResult, 0, len(items))
	for i, it := range items {
		r, err := PushToRemote(ctx, ch, store, it)
		if err != nil {
			fail := PushResult{
				OK:             false,
				Message:        err.Error(),
				ClientOutboxID: it.ClientOutboxID,
			}
			out = append(out, fail)
			return out, fmt.Errorf("item[%d]: %w", i, err)
		}
		out = append(out, *r)
	}
	return out, nil
}

func tableInChannel(ch *Channel, table string) bool {
	// Z4 冻结：通道 tables 仅历史兼容展示，push/pull 不再据此拒收。
	_ = ch
	return strings.TrimSpace(table) != ""
}

func normalizePushPayload(item PushItem, pkCol string) (op string, payload string, rowPK string, err error) {
	rawOp := strings.ToLower(strings.TrimSpace(item.Op))
	if rawOp == "" {
		rawOp = "upsert"
	}
	switch rawOp {
	case "delete":
		op = "delete"
	case "insert", "update", "update_by_id", "upsert":
		op = "upsert"
	default:
		return "", "", "", fmt.Errorf("unsupported op: %s", item.Op)
	}

	row := item.Row
	if row == nil && len(item.Rows) > 0 {
		row = item.Rows[0]
	}
	rowPK = strings.TrimSpace(item.RowPK)
	if row != nil {
		if rowPK == "" {
			rowPK = strings.TrimSpace(fmt.Sprint(row[pkCol]))
		}
		if op == "upsert" {
			// 保证 payload 含主键
			if _, ok := row[pkCol]; !ok && rowPK != "" {
				row = copyMap(row)
				row[pkCol] = rowPK
			}
		}
	}
	if rowPK == "" || rowPK == "<nil>" {
		return "", "", "", fmt.Errorf("row_pk required")
	}
	if op == "delete" && row == nil {
		row = map[string]any{pkCol: rowPK}
	}
	if row == nil {
		return "", "", "", fmt.Errorf("row required for %s", op)
	}
	b, err := json.Marshal(row)
	if err != nil {
		return "", "", "", err
	}
	return op, string(b), rowPK, nil
}

func copyMap(m map[string]any) map[string]any {
	out := make(map[string]any, len(m)+1)
	for k, v := range m {
		out[k] = v
	}
	return out
}
