package store

import (
	"context"
	"errors"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"github.com/sundynix/sundynix-shared/contract"
)

// KB 是一个知识库（按 owner 隔离 + 按 kind 组织：文件夹/项目/案件/通用）。
// 表名 sundynix_kb。(owner,name) 唯一 —— 同一用户下知识库名不重复。
// 向量/全文/图谱实际以 "owner/name" 作分区键，保证只有 owner 能查到自己的库。
type KB struct {
	BaseModel
	TenantID string `gorm:"size:64;index"` // 多租户作用域（gorm tenant 插件按上下文自动填/过滤）
	Owner    string `gorm:"size:64;uniqueIndex:idx_kb_owner_name"`
	Name     string `gorm:"size:64;uniqueIndex:idx_kb_owner_name"`
	Kind     string `gorm:"size:16"` // folder / project / case / general
}

func (KB) TableName() string  { return "sundynix_kb" }
func (KB) isTenantScoped()     {}

// ListKB 列出某 owner 的全部知识库（按创建时间）。
func (p *Postgres) ListKB(ctx context.Context, owner string) ([]KB, error) {
	if p.db == nil {
		return nil, nil
	}
	var rows []KB
	err := p.db.WithContext(ctx).Where("owner = ?", owner).Order("id").Find(&rows).Error
	return rows, err
}

// EnsureKB 幂等登记一个知识库（已存在则保持，不覆盖 kind）。
func (p *Postgres) EnsureKB(ctx context.Context, owner, name, kind string) error {
	if p.db == nil {
		return nil // 降级模式：不持久化注册表，不阻断入库
	}
	if kind == "" {
		kind = "general"
	}
	return p.db.WithContext(ctx).Clauses(clause.OnConflict{
		Columns:   []clause.Column{{Name: "owner"}, {Name: "name"}},
		DoNothing: true,
	}).Create(&KB{Owner: owner, Name: name, Kind: kind}).Error
}

// Agent 是一份保存的 Agent 编排（React Flow 图 JSON，按 owner 隔离）。
// 表名 sundynix_agent。(owner,name) 唯一 —— 同一用户下编排名不重复。
type Agent struct {
	BaseModel
	TenantID string `gorm:"size:64;index"` // 多租户作用域（tenant 插件自动填/过滤）
	Owner    string `gorm:"size:64;uniqueIndex:idx_agent_on"`
	Name     string `gorm:"size:128;uniqueIndex:idx_agent_on"`
	Graph    string `gorm:"type:text"` // {nodes,edges} 的 JSON（含布局）
}

func (Agent) TableName() string { return "sundynix_agent" }
func (Agent) isTenantScoped()   {}

// ListAgents 返回某 owner 的全部编排（最近更新在前）。
func (p *Postgres) ListAgents(ctx context.Context, owner string) ([]Agent, error) {
	if p.db == nil {
		return nil, nil
	}
	var rows []Agent
	err := p.db.WithContext(ctx).Where("owner = ?", owner).Order("updated_at desc").Find(&rows).Error
	return rows, err
}

// SaveAgent 新建/更新一份编排（owner+name 唯一，重名覆盖图与更新时间）。
func (p *Postgres) SaveAgent(ctx context.Context, owner, name, graph string) error {
	if p.db == nil {
		return errStoreDisabled
	}
	return p.db.WithContext(ctx).Clauses(clause.OnConflict{
		Columns:   []clause.Column{{Name: "owner"}, {Name: "name"}},
		DoUpdates: clause.Assignments(map[string]any{"graph": graph, "updated_at": time.Now()}),
	}).Create(&Agent{Owner: owner, Name: name, Graph: graph}).Error
}

// DeleteAgent 删除某 owner 的一份编排。
func (p *Postgres) DeleteAgent(ctx context.Context, owner, name string) error {
	if p.db == nil {
		return errStoreDisabled
	}
	return p.db.WithContext(ctx).Where("owner = ? AND name = ?", owner, name).Delete(&Agent{}).Error
}

// Doc 是入库的一份文件/笔记 —— 文件主表（供 Obsidian 式"文库"浏览：列表 + Markdown 阅读 + 双链）。
// 表名 sundynix_doc。(owner,kb,name) 唯一；按 owner 隔离。文档间关联一律用本表的雪花 ID 关联。
type Doc struct {
	BaseModel
	TenantID  string `gorm:"size:64;index"` // 多租户作用域（入库异步无请求 ctx，SaveDoc 按 owner 补）
	Owner     string `gorm:"size:64;uniqueIndex:idx_doc_okn"`
	KB        string `gorm:"size:64;uniqueIndex:idx_doc_okn"`
	Name      string `gorm:"size:160;uniqueIndex:idx_doc_okn"` // 文件名称（不含扩展名 / 笔记名）
	Ext       string `gorm:"size:16"`                          // 文件后缀（.md/.pdf/.docx…；笔记/文本为空）
	Size      int    // 原文字数（rune）
	Preview   string `gorm:"size:600"`  // 前若干字预览（列表/反链用，不拉全文）
	Content   string `gorm:"type:text"` // 正文一律落 MinIO 后置空；仅 MinIO 不可用时回退内联存这里
	ObjectKey string `gorm:"size:160"`  // 正文在 MinIO 的对象键（常态非空；空=回退内联 Content）
}

func (Doc) TableName() string { return "sundynix_doc" }
func (Doc) isTenantScoped()   {}

// DocLink 是文档间 [[双链]] 的索引（owner+kb 内 from→to），以 Doc.ID 关联，供反链/笔记关系图按 SQL 查询，
// 避免在前端扫全部正文。入库/编辑时按 from 文档重建其出链；目标尚未入库时 ToID 为空（悬空，记 ToName 待回填）。
type DocLink struct {
	BaseModel
	TenantID string `gorm:"size:64;index"` // 多租户作用域（ReplaceDocLinks 按 owner 补）
	Owner    string `gorm:"size:64;index:idx_link_of"`
	KB     string `gorm:"size:64;index:idx_link_of"`
	FromID string `gorm:"size:24;index:idx_link_of"` // 源文档 Doc.ID
	ToID   string `gorm:"size:24;index"`             // 目标文档 Doc.ID（空=悬空：目标尚未入库）
	ToName string `gorm:"size:160"`                  // [[原始名]]，供悬空链接展示 / 目标入库后回填 ToID
}

func (DocLink) TableName() string { return "sundynix_doc_link" }
func (DocLink) isTenantScoped()   {}

// SaveDoc 写入/更新一份文件（owner+kb+name 唯一，重名覆盖），返回 (文件雪花 ID, 被覆盖的旧对象键)。
// oldObjectKey 是重名覆盖前该文档的 ObjectKey（新建则为空）—— 供调用方清理 MinIO 孤儿对象。
// content 为内联正文（大文档转 MinIO 时传空 + objectKey）；ext/md5/preview/size 由调用方按全文给出。
func (p *Postgres) SaveDoc(ctx context.Context, owner, kb, name, ext, content, objectKey string, size int, preview string) (id, oldObjectKey string, err error) {
	if p.db == nil {
		return "", "", nil
	}
	// 入库为异步 worker（无请求 ctx 租户）→ 显式按 owner 补 tenant_id（插件在此不会自动填）。
	tenantID := p.tenantIDForOwner(ctx, owner)
	var d Doc
	qerr := p.db.WithContext(ctx).Where("owner = ? AND kb = ? AND name = ?", owner, kb, name).First(&d).Error
	if errors.Is(qerr, gorm.ErrRecordNotFound) {
		d = Doc{TenantID: tenantID, Owner: owner, KB: kb, Name: name, Ext: ext, Content: content, ObjectKey: objectKey, Size: size, Preview: preview}
		if err := p.db.WithContext(ctx).Create(&d).Error; err != nil {
			return "", "", err
		}
		return d.ID, "", nil
	}
	if qerr != nil {
		return "", "", qerr
	}
	oldObjectKey = d.ObjectKey // 覆盖前的旧对象键
	if tenantID != "" {
		d.TenantID = tenantID // 覆盖路径也补齐（老行可能为空）；查不到租户则不动，别把已有值抹空
	}
	d.Ext, d.Content, d.ObjectKey, d.Size, d.Preview = ext, content, objectKey, size, preview
	if err := p.db.WithContext(ctx).Save(&d).Error; err != nil {
		return "", "", err
	}
	return d.ID, oldObjectKey, nil
}

// ListVault 返回文库列表（仅元数据 + 预览，不含全文），避免一次拉回整库正文。
func (p *Postgres) ListVault(ctx context.Context, owner, kb string) ([]Doc, error) {
	if p.db == nil {
		return nil, nil
	}
	var rows []Doc
	err := p.db.WithContext(ctx).
		Select("id", "name", "ext", "size", "preview", "object_key", "updated_at").
		Where("owner = ? AND kb = ?", owner, kb).Order("updated_at desc").Find(&rows).Error
	return rows, err
}

// GetDocByID 按文件 ID 取单篇文档（含全文 Content 与 ObjectKey），owner 作用域防越权。
func (p *Postgres) GetDocByID(ctx context.Context, owner, id string) (*Doc, error) {
	if p.db == nil {
		return nil, nil
	}
	var d Doc
	if err := p.db.WithContext(ctx).Where("owner = ? AND id = ?", owner, id).First(&d).Error; err != nil {
		return nil, err
	}
	return &d, nil
}

// DeleteDocByID 删除一份文件的 PG 痕迹（owner 作用域防越权）：doc 行 + 其出链/入链。
// 入链（别处指向本文件）一并删，避免悬空脏链。文档不存在视作已删（返回 nil）。
func (p *Postgres) DeleteDocByID(ctx context.Context, owner, kb, id string) error {
	if p.db == nil {
		return nil
	}
	return p.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := tx.Where("owner = ? AND kb = ? AND (from_id = ? OR to_id = ?)", owner, kb, id, id).Delete(&DocLink{}).Error; err != nil {
			return err
		}
		return tx.Where("owner = ? AND id = ?", owner, id).Delete(&Doc{}).Error
	})
}

// ReplaceDocLinks 以源文件 ID 重建其出链（先删旧，再按 [[名称]] 解析目标 ID 后插新）—— 入库/编辑时调用。
// 目标文档尚未入库时 ToID 留空（悬空），待其入库时由 ResolveInboundLinks 回填。
func (p *Postgres) ReplaceDocLinks(ctx context.Context, owner, kb, fromID string, toNames []string) error {
	if p.db == nil {
		return nil
	}
	tenantID := p.tenantIDForOwner(ctx, owner) // 异步入库路径无请求 ctx 租户，按 owner 补
	return p.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := tx.Where("owner = ? AND kb = ? AND from_id = ?", owner, kb, fromID).Delete(&DocLink{}).Error; err != nil {
			return err
		}
		for _, name := range toNames {
			if name == "" {
				continue
			}
			var t Doc
			toID := ""
			if e := tx.Select("id").Where("owner = ? AND kb = ? AND name = ?", owner, kb, name).First(&t).Error; e == nil {
				toID = t.ID
			}
			if toID == fromID { // 自链跳过
				continue
			}
			if err := tx.Create(&DocLink{TenantID: tenantID, Owner: owner, KB: kb, FromID: fromID, ToID: toID, ToName: name}).Error; err != nil {
				return err
			}
		}
		return nil
	})
}

// ResolveInboundLinks 把指向 name 的悬空链接（ToID 空）回填为 id —— 目标文档入库后调用，使反链/关系图即时成形。
func (p *Postgres) ResolveInboundLinks(ctx context.Context, owner, kb, name, id string) error {
	if p.db == nil {
		return nil
	}
	return p.db.WithContext(ctx).Model(&DocLink{}).
		Where("owner = ? AND kb = ? AND to_name = ? AND (to_id = '' OR to_id IS NULL)", owner, kb, name).
		Update("to_id", id).Error
}

// ListLinks 返回某 kb 已解析（两端均为本库文件）的 [[双链]]（FromID→ToID），供反链/笔记关系图按 ID 渲染。
func (p *Postgres) ListLinks(ctx context.Context, owner, kb string) ([]DocLink, error) {
	if p.db == nil {
		return nil, nil
	}
	var rows []DocLink
	err := p.db.WithContext(ctx).Where("owner = ? AND kb = ? AND to_id <> ''", owner, kb).Find(&rows).Error
	return rows, err
}

// Pricing 是某模型的计价配置（token↔真钱）：按模型分输入/输出单价（每 1K token）。
// 表名 sundynix_pricing。ModelID 关联 sundynix_model.id，唯一。
type Pricing struct {
	BaseModel
	ModelID      string  `gorm:"size:24;uniqueIndex"`  // 关联 sundynix_model.id
	InputPer1K   float64 `gorm:"column:input_per_1k"`  // 每 1K 输入 token 单价
	OutputPer1K  float64 `gorm:"column:output_per_1k"` // 每 1K 输出 token 单价
	Currency     string  `gorm:"size:8"`               // 币种（CNY / USD…）
	CreditWeight float64 `gorm:"column:credit_weight"` // 积分权重（每 token 烧积分的倍率；0/缺省按 1.0 计）
}

func (Pricing) TableName() string { return "sundynix_pricing" }

// ListPricing 列出全部计价配置。
func (p *Postgres) ListPricing(ctx context.Context) ([]Pricing, error) {
	if p.db == nil {
		return nil, nil
	}
	var rows []Pricing
	err := p.db.WithContext(ctx).Find(&rows).Error
	return rows, err
}

// UpsertPricing 写入/更新某模型的计价（model_id 唯一，重复即覆盖单价/币种）。
func (p *Postgres) UpsertPricing(ctx context.Context, modelID string, inPer1K, outPer1K float64, currency string) error {
	if p.db == nil {
		return errStoreDisabled
	}
	return p.db.WithContext(ctx).Clauses(clause.OnConflict{
		Columns:   []clause.Column{{Name: "model_id"}},
		DoUpdates: clause.Assignments(map[string]any{"input_per_1k": inPer1K, "output_per_1k": outPer1K, "currency": currency, "updated_at": time.Now()}),
	}).Create(&Pricing{ModelID: modelID, InputPer1K: inPer1K, OutputPer1K: outPer1K, Currency: currency}).Error
}

// LLMModel 是一个模型后端配置（控制面：管理员在此登记可用模型）。
// 表名 sundynix_model（遵守前缀约定）。每个 kind 同一时刻仅一条 Active=true。
type LLMModel struct {
	BaseModel
	Kind     string `gorm:"size:16;index"` // chat / embedding
	Provider string `gorm:"size:32"`       // openai-compatible / vllm
	BaseURL  string `gorm:"size:255"`      // 如 https://api.deepseek.com
	APIKey   string `gorm:"size:255"`
	Model    string `gorm:"size:64"` // 如 deepseek-chat / text-embedding-v3
	Active   bool
}

func (LLMModel) TableName() string { return "sundynix_model" }

// ListModels 列出某 kind 的模型配置（kind 空则全部）。
func (p *Postgres) ListModels(ctx context.Context, kind string) ([]LLMModel, error) {
	if p.db == nil {
		return nil, nil
	}
	var rows []LLMModel
	q := p.db.WithContext(ctx).Order("id")
	if kind != "" {
		q = q.Where("kind = ?", kind)
	}
	err := q.Find(&rows).Error
	return rows, err
}

// SaveModel 新增或更新一条模型配置（ID 空则新增，BeforeCreate 生成雪花 ID）。
func (p *Postgres) SaveModel(ctx context.Context, m *LLMModel) error {
	if p.db == nil {
		return errStoreDisabled
	}
	if m.ID == "" {
		return p.db.WithContext(ctx).Create(m).Error
	}
	// 更新仅覆盖可编辑列：Active 由 SetActiveModel 单独管理，CreatedAt 不动——
	// 避免整行 Save 把 active 清零（改 key/base_url 时不应顺手取消激活）。
	return p.db.WithContext(ctx).Model(&LLMModel{}).Where("id = ?", m.ID).
		Select("Kind", "Provider", "BaseURL", "APIKey", "Model").Updates(m).Error
}

// SetActiveModel 把指定模型设为激活（同 kind 内其余取消），事务保证每 kind 唯一激活。
func (p *Postgres) SetActiveModel(ctx context.Context, id string) error {
	if p.db == nil {
		return errStoreDisabled
	}
	return p.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var m LLMModel
		if err := tx.First(&m, "id = ?", id).Error; err != nil {
			return err
		}
		if err := tx.Model(&LLMModel{}).Where("kind = ? AND active = ?", m.Kind, true).Update("active", false).Error; err != nil {
			return err
		}
		return tx.Model(&LLMModel{}).Where("id = ?", id).Update("active", true).Error
	})
}

// GetActiveModel 返回某 kind 当前激活模型（无则 nil）。
func (p *Postgres) GetActiveModel(ctx context.Context, kind string) (*LLMModel, error) {
	if p.db == nil {
		return nil, nil
	}
	var m LLMModel
	err := p.db.WithContext(ctx).Where("kind = ? AND active = ?", kind, true).First(&m).Error
	if err != nil {
		return nil, nil // 未配置激活模型
	}
	return &m, nil
}

// ActiveConfig 取某 kind 的激活模型配置；chat 还把"其它已登记 chat 模型"按序填进 Fallbacks，
// 供 dispatcher 串成 failover 链（主 provider 抖动/挂掉时自动切备，平台不整体宕）。
func (p *Postgres) ActiveConfig(ctx context.Context, kind string) *contract.ModelConfig {
	row, _ := p.GetActiveModel(ctx, kind)
	if row == nil {
		return nil
	}
	cfg := &contract.ModelConfig{Provider: row.Provider, BaseURL: row.BaseURL, APIKey: row.APIKey, Model: row.Model}
	if kind == contract.ConfigKindChat {
		all, _ := p.ListModels(ctx, kind)
		for _, m := range all {
			if m.ID == row.ID {
				continue // 跳过激活模型（它已是主）
			}
			cfg.Fallbacks = append(cfg.Fallbacks, contract.ModelConfig{
				Provider: m.Provider, BaseURL: m.BaseURL, APIKey: m.APIKey, Model: m.Model,
			})
		}
	}
	return cfg
}

// DeleteModel 删除一条模型配置。
func (p *Postgres) DeleteModel(ctx context.Context, id string) error {
	if p.db == nil {
		return errStoreDisabled
	}
	return p.db.WithContext(ctx).Delete(&LLMModel{}, "id = ?", id).Error
}
