package models

import (
	"database/sql/driver"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"
)

// StringList / IntList are JSON-encoded slice columns, stored as TEXT so they
// work identically on sqlite and mysql.
type StringList []string

func (l StringList) Value() (driver.Value, error) {
	if l == nil {
		return "[]", nil
	}
	b, err := json.Marshal([]string(l))
	if err != nil {
		return nil, err
	}
	return string(b), nil
}

func (l *StringList) Scan(v any) error {
	if v == nil {
		*l = StringList{}
		return nil
	}
	var b []byte
	switch t := v.(type) {
	case []byte:
		b = t
	case string:
		b = []byte(t)
	default:
		return errors.New("StringList: unsupported scan type")
	}
	if len(b) == 0 {
		*l = StringList{}
		return nil
	}
	return json.Unmarshal(b, (*[]string)(l))
}

type IntList []int

func (l IntList) Value() (driver.Value, error) {
	if l == nil {
		return "[]", nil
	}
	b, err := json.Marshal([]int(l))
	if err != nil {
		return nil, err
	}
	return string(b), nil
}

func (l *IntList) Scan(v any) error {
	if v == nil {
		*l = IntList{}
		return nil
	}
	var b []byte
	switch t := v.(type) {
	case []byte:
		b = t
	case string:
		b = []byte(t)
	default:
		return errors.New("IntList: unsupported scan type")
	}
	if len(b) == 0 {
		*l = IntList{}
		return nil
	}
	return json.Unmarshal(b, (*[]int)(l))
}

type Team struct {
	ID          uint   `gorm:"primaryKey" json:"id"`
	Name        string `gorm:"size:128;uniqueIndex" json:"name"`
	DisplayName string `gorm:"size:255" json:"display_name"`
	// GroupName is the legacy single-group column, kept only for one-time backfill
	// into GroupNames; it is no longer read by application logic.
	GroupName string `gorm:"column:group_name" json:"-"`
	// GroupNames is the set of groups a team belongs to (shown under each group tab).
	GroupNames StringList `gorm:"type:text" json:"group_names"`
	// SortPriority controls display order within a group: lower shows first
	// (ascending), ties broken by display name.
	SortPriority int    `gorm:"default:0" json:"sort_priority"`
	Description  string `gorm:"type:text" json:"description"`
	IsDisabled   bool   `json:"is_disabled"`
	// BackupEnabled turns on the implicit backup lane (= previous shift's primary).
	BackupEnabled bool   `gorm:"default:false" json:"backup_enabled"`
	FeishuChatID  string `gorm:"size:64" json:"feishu_chat_id"`
}

// TeamLayer is one rotation layer of a team's schedule.
// effective = highest-priority layer whose active window covers the instant;
// within that layer people rotate every RotateEverySeconds, OnCallAtOnce at a time.
type TeamLayer struct {
	ID                 uint       `gorm:"primaryKey" json:"id"`
	TeamID             uint       `gorm:"index" json:"team_id"`
	Priority           int        `gorm:"index" json:"priority"`
	People             StringList `gorm:"type:text" json:"people"`
	RotateEverySeconds int64      `json:"rotate_every_seconds"`
	OnCallAtOnce       int        `gorm:"default:1" json:"on_call_at_once"`
	// CombineMode controls how this layer combines with lower-priority ones when
	// both are active: "override" (default) seals off everything below it;
	// "append" unions its people on top and lets lower layers keep contributing.
	CombineMode string `gorm:"size:16;default:override" json:"combine_mode"`
	// SourceTeamID, when > 0, makes this a reference layer: instead of People/
	// rotation, its on-call is the referenced team's current primary on-call.
	// A team's layers must be all-people or all-reference (no mix); referenced
	// teams must be people-only (no nesting), so references never recurse/cycle.
	SourceTeamID uint `gorm:"index" json:"source_team_id"`
	// ActiveMode: "always" | "window". For "window" the layer is active only on
	// ActiveDays (0=Sun..6=Sat) between ActiveStartMin..ActiveEndMin (minutes of
	// day, fixed UTC+8). ActiveEndMin < ActiveStartMin means the window crosses
	// midnight.
	ActiveMode     string    `gorm:"size:16;default:always" json:"active_mode"`
	ActiveDays     IntList   `gorm:"type:text" json:"active_days"`
	ActiveStartMin int       `json:"active_start_min"`
	ActiveEndMin   int       `json:"active_end_min"`
	StartsAt       int64     `json:"starts_at"`
	CreatedAt      time.Time `json:"created_at"`
	UpdatedAt      time.Time `json:"updated_at"`
	Team           Team      `json:"-" gorm:"foreignKey:TeamID"`
}

// RealizedShift is the frozen Layer baseline for a past shift. It records what
// the rotation produced (no overrides — those stay in ShiftInterval and are
// merged live at read time). Written once a shift ends; immutable thereafter.
type RealizedShift struct {
	ID            uint   `gorm:"primaryKey" json:"id"`
	TeamID        uint   `gorm:"uniqueIndex:idx_realized_uniq,priority:1;index:idx_realized_team_time,priority:1" json:"team_id"`
	StartTime     int64  `gorm:"uniqueIndex:idx_realized_uniq,priority:2;index:idx_realized_team_time,priority:2" json:"start_time"`
	EndTime       int64  `gorm:"uniqueIndex:idx_realized_uniq,priority:3;index:idx_realized_team_time,priority:3" json:"end_time"`
	EngineerEmail string `gorm:"size:191;uniqueIndex:idx_realized_uniq,priority:4" json:"engineer_email"`
	IsBackup      bool   `gorm:"uniqueIndex:idx_realized_uniq,priority:5;default:false" json:"is_backup"`
	FrozenAt      int64  `json:"frozen_at"`
}

type Engineer struct {
	ID        uint   `gorm:"primaryKey" json:"id"`
	Name      string `json:"name"`
	EnName    string `json:"en_name"`
	Email     string `gorm:"size:191;uniqueIndex" json:"email"`
	Phone     string `json:"phone"`
	PagePhone string `gorm:"size:32" json:"page_phone"`
}

// Team roles form a ladder: a member reads the schedule and may take an
// on-call slot themselves, an editor may additionally change the schedule and
// the member list, and a manager may on top of that hand out roles and disable
// the team. Rank comparisons (TeamRoleRank) are what authorization checks use,
// so a manager implicitly has every editor right.
const (
	TeamRoleMember  = "member"
	TeamRoleEditor  = "editor"
	TeamRoleManager = "manager"
)

// TeamRoleRank orders the ladder. An empty or unrecognized role ranks as
// member, so a row written before the column existed is read-only by default.
func TeamRoleRank(role string) int {
	switch strings.ToLower(strings.TrimSpace(role)) {
	case TeamRoleManager:
		return 3
	case TeamRoleEditor:
		return 2
	default:
		return 1
	}
}

// NormalizeTeamRole canonicalizes a caller-supplied role name and rejects
// anything outside the ladder.
func NormalizeTeamRole(role string) (string, error) {
	switch r := strings.ToLower(strings.TrimSpace(role)); r {
	case TeamRoleMember, TeamRoleEditor, TeamRoleManager:
		return r, nil
	case "":
		return "", errors.New("missing role")
	default:
		return "", fmt.Errorf("invalid role %q: want one of member, editor, manager", role)
	}
}

type EngineerTeam struct {
	ID         uint   `gorm:"primaryKey" json:"id"`
	EngineerID uint   `json:"engineer_id"`
	TeamID     uint   `json:"team_id"`
	Role       string `gorm:"size:32;default:member" json:"role"`
	// IsManager is the legacy boolean this table used before Role existed. It
	// is kept only as a mirror so a rollback to an older binary still sees the
	// right managers; it is written in exactly one place (service.setMemberRole)
	// and never read for authorization. Use Role for that.
	IsManager bool     `json:"is_manager"`
	Engineer  Engineer `json:"engineer" gorm:"foreignKey:EngineerID"`
	Team      Team     `json:"team" gorm:"foreignKey:TeamID"`
}

// HasRole reports whether the link is at least as privileged as min.
func (et EngineerTeam) HasRole(min string) bool {
	return TeamRoleRank(et.Role) >= TeamRoleRank(min)
}

type ShiftInterval struct {
	ID         uint      `gorm:"primaryKey" json:"id"`
	TeamID     uint      `gorm:"index:idx_interval_team_time,priority:1" json:"team_id"`
	EngineerID uint      `gorm:"index" json:"engineer_id"`
	StartTime  int64     `gorm:"index:idx_interval_team_time,priority:2" json:"start_time"`
	EndTime    int64     `gorm:"index:idx_interval_team_time,priority:3" json:"end_time"`
	Overlay    int       `gorm:"index;default:0" json:"overlay"`
	IsBackup   bool      `gorm:"index;default:false" json:"is_backup"`
	SourceType string    `gorm:"size:64" json:"source_type"`
	IsOverride bool      `json:"is_override"`
	CreatedBy  string    `gorm:"size:191" json:"created_by"`
	CreatedAt  time.Time `json:"created_at"`
	UpdatedAt  time.Time `json:"updated_at"`
	Team       Team      `json:"team" gorm:"foreignKey:TeamID"`
	Engineer   Engineer  `json:"engineer" gorm:"foreignKey:EngineerID"`
}

type AuditLog struct {
	ID           uint      `gorm:"primaryKey" json:"id"`
	RequestID    string    `json:"request_id"`
	UserEmail    string    `json:"user_email"`
	UserName     string    `json:"user_name"`
	Method       string    `json:"method"`
	Path         string    `json:"path"`
	Action       string    `json:"action"`
	TeamID       uint      `gorm:"column:team_id" json:"team_id"`
	StatusCode   int       `json:"status_code"`
	Success      bool      `json:"success"`
	RequestBody  string    `gorm:"type:text" json:"request_body"`
	ResponseBody string    `gorm:"type:text" json:"response_body"`
	ErrorMessage string    `gorm:"type:text" json:"error_message"`
	CreatedAt    time.Time `json:"created_at"`
}

// NotificationLog keeps recurring jobs idempotent across ticks/restarts/replicas:
// the sender claims a slot via the unique (Kind, RefTime) key before sending.
// RefTime is the instant the job is keyed on (e.g. the roster's send time), not when it ran.
type NotificationLog struct {
	ID      uint   `gorm:"primaryKey" json:"id"`
	Kind    string `gorm:"size:64;uniqueIndex:idx_notif_uniq,priority:1" json:"kind"`
	RefTime int64  `gorm:"uniqueIndex:idx_notif_uniq,priority:2" json:"ref_time"`
	SentAt  int64  `json:"sent_at"`
	// MessageID is the Feishu message_id returned when this notification was sent
	// as a card, so a later tick can patch its content in place. Nil for
	// plain-text sends or if persisting the ID failed after a successful send.
	MessageID *string `gorm:"size:64" json:"message_id,omitempty"`
	// ContentHash is a hash of the last rendered card content (sent or patched),
	// used to skip no-op patches when nothing has changed.
	ContentHash *string `gorm:"size:64" json:"content_hash,omitempty"`
	// PatchedAt is the unix time of the last successful card patch, if any.
	PatchedAt *int64 `json:"patched_at,omitempty"`
}

// API key principal types.
const (
	ApiKeyTypeUser    = "user"    // Acts as a human; inherits that user's permissions (no scope restriction).
	ApiKeyTypeService = "service" // Machine-to-machine principal; restricted to its explicit Scopes.
)

type ApiKey struct {
	ID        uint      `gorm:"primaryKey" json:"id"`
	KeyHash   string    `gorm:"size:191;uniqueIndex" json:"-"`   // sha256 hex of the key; plaintext is shown only once at creation
	KeyPrefix string    `gorm:"size:32;index" json:"key_prefix"` // Non-secret display hint, e.g. oc_v2_1a2b3c4d
	Email     string    `gorm:"size:191;index" json:"email"`     // Owner email (creator). For service keys this is the creating admin.
	Name      string    `json:"name"`                            // Description/Name (for service keys: the service identity)
	Type      string    `gorm:"size:16;index" json:"type"`       // "user" | "service"
	Scopes    string    `gorm:"size:255" json:"scopes"`          // Space-separated scopes (service keys only), e.g. "oncall:read"
	CreatedAt time.Time `json:"created_at"`
}

type SuperAdmin struct {
	ID        uint      `gorm:"primaryKey" json:"id"`
	Email     string    `gorm:"size:191;uniqueIndex" json:"email"`
	CreatedBy string    `gorm:"size:191" json:"created_by"`
	CreatedAt time.Time `json:"created_at"`
}

// New User model for email/password auth
type User struct {
	ID           uint      `gorm:"primaryKey" json:"id"`
	Email        string    `gorm:"size:191;uniqueIndex" json:"email"`
	PasswordHash string    `json:"-"` // Never return in JSON
	Name         string    `json:"name"`
	CreatedAt    time.Time `json:"created_at"`
	UpdatedAt    time.Time `json:"updated_at"`
}

// Session model for JWT-like session tokens
type UserSession struct {
	ID        uint      `gorm:"primaryKey" json:"id"`
	UserID    uint      `json:"user_id"`
	Token     string    `gorm:"size:191;uniqueIndex" json:"-"` // Never return in JSON
	CreatedAt time.Time `json:"created_at"`
	ExpiresAt time.Time `json:"expires_at"`
	User      User      `json:"user" gorm:"foreignKey:UserID"`
}
