package storage

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

type AssetRecord struct {
    Name         string
    Description  string
    TemplateName string
    BusinessName string
    BodyJSON     string
    CreatedAt    string
    UpdatedAt    string
}

type IntegrationServiceRecord struct {
    Name        string
    ServiceType string
    Description string
    Enabled     bool
    BodyJSON    string
    CreatedAt   string
    UpdatedAt   string
}

type VideoSourceRecord struct {
    Name        string
    SourceType  string
    Area        string
    Description string
    BodyJSON    string
    CreatedAt   string
    UpdatedAt   string
}

type DeviceAssignmentRecord struct {
    DeviceID     string
    ProfileName  string
    Description  string
    BodyJSON     string
    CreatedAt    string
    UpdatedAt    string
}

type RecognitionUnitRecord struct {
    SceneTemplateName string
    Name              string
    DisplayName       string
    SiteName          string
    VideoSourceRef    string
    OutputChannel     string
    RTSPPort          string
    Description       string
    BodyJSON          string
    CreatedAt         string
    UpdatedAt         string
}

type AssetsRepo struct {
    db *sql.DB
}

func NewAssetsRepo(db *sql.DB) *AssetsRepo {
    return &AssetsRepo{db: db}
}

func (r *AssetsRepo) SaveTemplate(name string, description string, bodyJSON string) error {
    return r.saveAsset("templates", AssetRecord{
        Name:        name,
        Description: description,
        BodyJSON:    bodyJSON,
    })
}

func (r *AssetsRepo) SaveProfile(name string, templateName string, businessName string, description string, bodyJSON string) error {
    return r.saveAsset("profiles", AssetRecord{
        Name:         name,
        TemplateName: templateName,
        BusinessName: businessName,
        Description:  description,
        BodyJSON:     bodyJSON,
    })
}

func (r *AssetsRepo) SaveOverlay(name string, description string, bodyJSON string) error {
    return r.saveAsset("overlays", AssetRecord{
        Name:        name,
        Description: description,
        BodyJSON:    bodyJSON,
    })
}

func (r *AssetsRepo) SaveIntegrationService(name string, serviceType string, description string, enabled bool, bodyJSON string) error {
    if r == nil || r.db == nil {
        return nil
    }
    now := time.Now().Format(time.RFC3339)
    _, err := r.db.Exec(`
INSERT INTO integration_services(name, type, description, enabled, body_json, created_at, updated_at)
VALUES(?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM integration_services WHERE name = ?), ?), ?)
ON CONFLICT(name) DO UPDATE SET
  type=excluded.type,
  description=excluded.description,
  enabled=excluded.enabled,
  body_json=excluded.body_json,
  updated_at=excluded.updated_at
`, name, serviceType, description, enabled, bodyJSON, name, now, now)
    return err
}

func (r *AssetsRepo) SaveVideoSource(name string, sourceType string, area string, description string, bodyJSON string) error {
    if r == nil || r.db == nil {
        return nil
    }
    now := time.Now().Format(time.RFC3339)
    _, err := r.db.Exec(`
INSERT INTO video_sources(name, source_type, area, description, body_json, created_at, updated_at)
VALUES(?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM video_sources WHERE name = ?), ?), ?)
ON CONFLICT(name) DO UPDATE SET
  source_type=excluded.source_type,
  area=excluded.area,
  description=excluded.description,
  body_json=excluded.body_json,
  updated_at=excluded.updated_at
`, name, sourceType, area, description, bodyJSON, name, now, now)
    return err
}

func (r *AssetsRepo) SaveDeviceAssignment(deviceID string, profileName string, description string, bodyJSON string) error {
    if r == nil || r.db == nil {
        return nil
    }
    now := time.Now().Format(time.RFC3339)
    _, err := r.db.Exec(`
INSERT INTO device_assignments(device_id, profile_name, description, body_json, created_at, updated_at)
VALUES(?, ?, ?, ?, COALESCE((SELECT created_at FROM device_assignments WHERE device_id = ?), ?), ?)
ON CONFLICT(device_id) DO UPDATE SET
  profile_name=excluded.profile_name,
  description=excluded.description,
  body_json=excluded.body_json,
  updated_at=excluded.updated_at
`, deviceID, profileName, description, bodyJSON, deviceID, now, now)
    return err
}

func (r *AssetsRepo) SaveRecognitionUnit(record RecognitionUnitRecord) error {
    if r == nil || r.db == nil {
        return nil
    }
    now := time.Now().Format(time.RFC3339)
    _, err := r.db.Exec(`
INSERT INTO recognition_units(scene_template_name, name, display_name, site_name, video_source_ref, output_channel, rtsp_port, description, body_json, created_at, updated_at)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM recognition_units WHERE scene_template_name = ? AND name = ?), ?), ?)
ON CONFLICT(scene_template_name, name) DO UPDATE SET
  display_name=excluded.display_name,
  site_name=excluded.site_name,
  video_source_ref=excluded.video_source_ref,
  output_channel=excluded.output_channel,
  rtsp_port=excluded.rtsp_port,
  description=excluded.description,
  body_json=excluded.body_json,
  updated_at=excluded.updated_at
`, record.SceneTemplateName, record.Name, record.DisplayName, record.SiteName, record.VideoSourceRef, record.OutputChannel, record.RTSPPort, record.Description, record.BodyJSON, record.SceneTemplateName, record.Name, now, now)
    return err
}

func (r *AssetsRepo) ListTemplates() ([]AssetRecord, error) {
    return r.listAssets("templates")
}

func (r *AssetsRepo) ListProfiles() ([]AssetRecord, error) {
    return r.listAssets("profiles")
}

func (r *AssetsRepo) ListOverlays() ([]AssetRecord, error) {
    return r.listAssets("overlays")
}

func (r *AssetsRepo) ListIntegrationServices() ([]IntegrationServiceRecord, error) {
    if r == nil || r.db == nil {
        return nil, nil
    }
    rows, err := r.db.Query(`
SELECT name, type, description, enabled, body_json, created_at, updated_at
FROM integration_services
ORDER BY updated_at DESC, name ASC
`)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var out []IntegrationServiceRecord
    for rows.Next() {
        var item IntegrationServiceRecord
        if err := rows.Scan(&item.Name, &item.ServiceType, &item.Description, &item.Enabled, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt); err != nil {
            return nil, err
        }
        out = append(out, item)
    }
    return out, rows.Err()
}

func (r *AssetsRepo) ListVideoSources() ([]VideoSourceRecord, error) {
    if r == nil || r.db == nil {
        return nil, nil
    }
    rows, err := r.db.Query(`
SELECT name, source_type, area, description, body_json, created_at, updated_at
FROM video_sources
ORDER BY updated_at DESC, name ASC
`)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var out []VideoSourceRecord
    for rows.Next() {
        var item VideoSourceRecord
        if err := rows.Scan(&item.Name, &item.SourceType, &item.Area, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt); err != nil {
            return nil, err
        }
        out = append(out, item)
    }
    return out, rows.Err()
}

func (r *AssetsRepo) ListDeviceAssignments() ([]DeviceAssignmentRecord, error) {
    if r == nil || r.db == nil {
        return nil, nil
    }
    rows, err := r.db.Query(`
SELECT device_id, profile_name, description, body_json, created_at, updated_at
FROM device_assignments
ORDER BY updated_at DESC, device_id ASC
`)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var out []DeviceAssignmentRecord
    for rows.Next() {
        var item DeviceAssignmentRecord
        if err := rows.Scan(&item.DeviceID, &item.ProfileName, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt); err != nil {
            return nil, err
        }
        out = append(out, item)
    }
    return out, rows.Err()
}

func (r *AssetsRepo) ListRecognitionUnits() ([]RecognitionUnitRecord, error) {
    if r == nil || r.db == nil {
        return nil, nil
    }
    rows, err := r.db.Query(`
SELECT scene_template_name, name, display_name, site_name, video_source_ref, output_channel, rtsp_port, description, body_json, created_at, updated_at
FROM recognition_units
ORDER BY scene_template_name ASC, name ASC
`)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    var out []RecognitionUnitRecord
    for rows.Next() {
        var item RecognitionUnitRecord
        if err := rows.Scan(&item.SceneTemplateName, &item.Name, &item.DisplayName, &item.SiteName, &item.VideoSourceRef, &item.OutputChannel, &item.RTSPPort, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt); err != nil {
            return nil, err
        }
        out = append(out, item)
    }
    return out, rows.Err()
}

func (r *AssetsRepo) GetTemplate(name string) (*AssetRecord, error) {
    return r.getAsset("templates", name)
}

func (r *AssetsRepo) GetProfile(name string) (*AssetRecord, error) {
    return r.getAsset("profiles", name)
}

func (r *AssetsRepo) GetOverlay(name string) (*AssetRecord, error) {
    return r.getAsset("overlays", name)
}

func (r *AssetsRepo) GetIntegrationService(name string) (*IntegrationServiceRecord, error) {
    if r == nil || r.db == nil {
        return nil, nil
    }
    var item IntegrationServiceRecord
    err := r.db.QueryRow(`
SELECT name, type, description, enabled, body_json, created_at, updated_at
FROM integration_services
WHERE name = ?
`, name).Scan(&item.Name, &item.ServiceType, &item.Description, &item.Enabled, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    if err != nil {
        return nil, err
    }
    return &item, nil
}

func (r *AssetsRepo) GetVideoSource(name string) (*VideoSourceRecord, error) {
    if r == nil || r.db == nil {
        return nil, nil
    }
    var item VideoSourceRecord
    err := r.db.QueryRow(`
SELECT name, source_type, area, description, body_json, created_at, updated_at
FROM video_sources
WHERE name = ?
`, name).Scan(&item.Name, &item.SourceType, &item.Area, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    if err != nil {
        return nil, err
    }
    return &item, nil
}

func (r *AssetsRepo) GetDeviceAssignment(deviceID string) (*DeviceAssignmentRecord, error) {
    if r == nil || r.db == nil {
        return nil, nil
    }
    var item DeviceAssignmentRecord
    err := r.db.QueryRow(`
SELECT device_id, profile_name, description, body_json, created_at, updated_at
FROM device_assignments
WHERE device_id = ?
`, deviceID).Scan(&item.DeviceID, &item.ProfileName, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    if err != nil {
        return nil, err
    }
    return &item, nil
}

func (r *AssetsRepo) GetRecognitionUnit(sceneTemplateName string, name string) (*RecognitionUnitRecord, error) {
    if r == nil || r.db == nil {
        return nil, nil
    }
    var item RecognitionUnitRecord
    err := r.db.QueryRow(`
SELECT scene_template_name, name, display_name, site_name, video_source_ref, output_channel, rtsp_port, description, body_json, created_at, updated_at
FROM recognition_units
WHERE scene_template_name = ? AND name = ?
`, sceneTemplateName, name).Scan(&item.SceneTemplateName, &item.Name, &item.DisplayName, &item.SiteName, &item.VideoSourceRef, &item.OutputChannel, &item.RTSPPort, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    if err != nil {
        return nil, err
    }
    return &item, nil
}

func (r *AssetsRepo) DeleteTemplate(name string) error {
    return r.deleteAsset("templates", name)
}

func (r *AssetsRepo) DeleteProfile(name string) error {
    return r.deleteAsset("profiles", name)
}

func (r *AssetsRepo) DeleteIntegrationService(name string) error {
    if r == nil || r.db == nil {
        return nil
    }
    _, err := r.db.Exec(`DELETE FROM integration_services WHERE name = ?`, name)
    return err
}

func (r *AssetsRepo) DeleteVideoSource(name string) error {
    if r == nil || r.db == nil {
        return nil
    }
    _, err := r.db.Exec(`DELETE FROM video_sources WHERE name = ?`, name)
    return err
}

func (r *AssetsRepo) DeleteDeviceAssignment(deviceID string) error {
    if r == nil || r.db == nil {
        return nil
    }
    _, err := r.db.Exec(`DELETE FROM device_assignments WHERE device_id = ?`, deviceID)
    return err
}

func (r *AssetsRepo) DeleteRecognitionUnit(sceneTemplateName string, name string) error {
    if r == nil || r.db == nil {
        return nil
    }
    _, err := r.db.Exec(`DELETE FROM recognition_units WHERE scene_template_name = ? AND name = ?`, sceneTemplateName, name)
    return err
}

func (r *AssetsRepo) DeleteOverlay(name string) error {
    return r.deleteAsset("overlays", name)
}

func (r *AssetsRepo) RenameTemplate(oldName string, newName string, description string, bodyJSON string) error {
    if r == nil || r.db == nil {
        return nil
    }
    oldName = strings.TrimSpace(oldName)
    newName = strings.TrimSpace(newName)
    if oldName == "" || newName == "" {
        return fmt.Errorf("template name is required")
    }
    if oldName == newName {
        return r.SaveTemplate(newName, description, bodyJSON)
    }

    tx, err := r.db.Begin()
    if err != nil {
        return err
    }
    defer func() {
        if tx != nil {
            _ = tx.Rollback()
        }
    }()

    var exists int
    if err := tx.QueryRow(`SELECT COUNT(1) FROM templates WHERE name = ?`, oldName).Scan(&exists); err != nil {
        return err
    }
    if exists == 0 {
        return sql.ErrNoRows
    }
    if err := tx.QueryRow(`SELECT COUNT(1) FROM templates WHERE name = ?`, newName).Scan(&exists); err != nil {
        return err
    }
    if exists > 0 {
        return fmt.Errorf("template %q already exists", newName)
    }

    now := time.Now().Format(time.RFC3339)
    if _, err := tx.Exec(`
INSERT INTO templates(name, description, body_json, created_at, updated_at)
SELECT ?, ?, ?, created_at, ?
FROM templates
WHERE name = ?
`, newName, description, bodyJSON, now, oldName); err != nil {
        return err
    }

    rows, err := tx.Query(`
SELECT name, description, business_name, body_json
FROM scene_templates
WHERE primary_template_name = ? OR body_json LIKE ?
`, oldName, "%\"primary_template_name\": \""+oldName+"\"%")
    if err != nil {
        return err
    }
    defer rows.Close()

    type profileUpdate struct {
        name         string
        description  string
        businessName string
        bodyJSON     string
    }
    updates := make([]profileUpdate, 0)
    for rows.Next() {
        var item profileUpdate
        if err := rows.Scan(&item.name, &item.description, &item.businessName, &item.bodyJSON); err != nil {
            return err
        }
        rewritten, changed, err := rewriteProfileTemplateRefs(item.bodyJSON, oldName, newName)
        if err != nil {
            return err
        }
        if changed {
            item.bodyJSON = rewritten
        }
        updates = append(updates, item)
    }
    if err := rows.Err(); err != nil {
        return err
    }
    for _, item := range updates {
        if _, err := tx.Exec(`
UPDATE scene_templates
SET primary_template_name = ?, body_json = ?, updated_at = ?
WHERE name = ?
`, newName, item.bodyJSON, now, item.name); err != nil {
            return err
        }
    }

    if _, err := tx.Exec(`
UPDATE recognition_units
SET body_json = REPLACE(body_json, ?, ?), updated_at = ?
WHERE body_json LIKE ?
`, `"`+"template"+`": "`+oldName+`"`, `"`+"template"+`": "`+newName+`"`, now, "%\"template\": \""+oldName+"\"%"); err != nil {
        return err
    }

    if _, err := tx.Exec(`DELETE FROM templates WHERE name = ?`, oldName); err != nil {
        return err
    }
    if err := tx.Commit(); err != nil {
        return err
    }
    tx = nil
    return nil
}

func (r *AssetsRepo) saveAsset(table string, record AssetRecord) error {
    if r == nil || r.db == nil {
        fmt.Printf("[DB] saveAsset SKIP nil table=%s\n", table)
        return nil
    }
    fmt.Printf("[DB] saveAsset table=%s name=%s\n", table, record.Name)
    now := time.Now().Format(time.RFC3339)
    switch table {
    case "templates", "overlays":
        _, err := r.db.Exec(`
INSERT INTO `+table+`(name, description, body_json, created_at, updated_at)
VALUES(?, ?, ?, COALESCE((SELECT created_at FROM `+table+` WHERE name = ?), ?), ?)
ON CONFLICT(name) DO UPDATE SET
  description=excluded.description,
  body_json=excluded.body_json,
  updated_at=excluded.updated_at
`, record.Name, record.Description, record.BodyJSON, record.Name, now, now)
        return err
    case "profiles":
        normalized, units, replaceUnits, err := normalizeProfileRecord(record)
        if err != nil {
            return err
        }
        // Save profile row (atomic Exec, no transaction).
        _, err = r.db.Exec(`
INSERT INTO scene_templates(name, primary_template_name, business_name, description, body_json, created_at, updated_at)
VALUES(?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM scene_templates WHERE name = ?), ?), ?)
ON CONFLICT(name) DO UPDATE SET
  primary_template_name=excluded.primary_template_name,
  business_name=excluded.business_name,
  description=excluded.description,
  body_json=excluded.body_json,
  updated_at=excluded.updated_at
`, normalized.Name, normalized.TemplateName, normalized.BusinessName, normalized.Description, normalized.BodyJSON, normalized.Name, now, now)
        if err != nil {
            return err
        }
        // Recognition units are saved separately via SaveRecognitionUnit.
        // Only do replace logic here when called from the old profile editor UI
        // (where units are embedded in the profile JSON and not saved separately).
        if replaceUnits {
            if _, err := r.db.Exec(`DELETE FROM recognition_units WHERE scene_template_name = ?`, normalized.Name); err != nil {
                return err
            }
            for _, unit := range units {
                if _, err := r.db.Exec(`
INSERT INTO recognition_units(scene_template_name, name, display_name, site_name, video_source_ref, output_channel, rtsp_port, description, body_json, created_at, updated_at)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM recognition_units WHERE scene_template_name = ? AND name = ?), ?), ?)
ON CONFLICT(scene_template_name, name) DO UPDATE SET
  display_name=excluded.display_name,
  site_name=excluded.site_name,
  video_source_ref=excluded.video_source_ref,
  output_channel=excluded.output_channel,
  rtsp_port=excluded.rtsp_port,
  description=excluded.description,
  body_json=excluded.body_json,
  updated_at=excluded.updated_at
`, unit.SceneTemplateName, unit.Name, unit.DisplayName, unit.SiteName, unit.VideoSourceRef, unit.OutputChannel, unit.RTSPPort, unit.Description, unit.BodyJSON, unit.SceneTemplateName, unit.Name, now, now); err != nil {
                    return err
                }
            }
        }
        return nil
    default:
        return nil
    }
}

func normalizeProfileRecord(record AssetRecord) (AssetRecord, []RecognitionUnitRecord, bool, error) {
    raw := map[string]any{}
    if strings.TrimSpace(record.BodyJSON) != "" {
        if err := json.Unmarshal([]byte(record.BodyJSON), &raw); err != nil {
            return AssetRecord{}, nil, false, err
        }
    }
    if raw == nil {
        raw = map[string]any{}
    }
    _, hasInstances := raw["instances"]
    if !hasInstances {
        return record, nil, false, nil
    }
    templateDoc, units, err := splitLegacyProfileDocument(struct {
        Name         string
        TemplateName string
        BusinessName string
        Description  string
        BodyJSON     string
        CreatedAt    string
        UpdatedAt    string
    }{
        Name:         record.Name,
        TemplateName: record.TemplateName,
        BusinessName: record.BusinessName,
        Description:  record.Description,
        BodyJSON:     record.BodyJSON,
        CreatedAt:    record.CreatedAt,
        UpdatedAt:    record.UpdatedAt,
    })
    if err != nil {
        return AssetRecord{}, nil, false, err
    }
    body, err := json.MarshalIndent(templateDoc, "", "  ")
    if err != nil {
        return AssetRecord{}, nil, false, err
    }
    normalized := record
    normalized.BodyJSON = string(append(body, '\n'))
    outUnits := make([]RecognitionUnitRecord, 0, len(units))
    for _, unit := range units {
        outUnits = append(outUnits, RecognitionUnitRecord{
            SceneTemplateName: record.Name,
            Name:              unit.Name,
            DisplayName:       unit.DisplayName,
            SiteName:          unit.SiteName,
            VideoSourceRef:    unit.VideoSourceRef,
            OutputChannel:     unit.OutputChannel,
            RTSPPort:          unit.RTSPPort,
            Description:       record.Description,
            BodyJSON:          unit.BodyJSON,
            CreatedAt:         record.CreatedAt,
            UpdatedAt:         record.UpdatedAt,
        })
    }
    return normalized, outUnits, true, nil
}

func (r *AssetsRepo) listAssets(table string) ([]AssetRecord, error) {
    if r == nil || r.db == nil {
        return nil, nil
    }
    query := `
SELECT name, description, body_json, created_at, updated_at, '', ''
FROM ` + table + `
ORDER BY updated_at DESC, name ASC
`
    if table == "profiles" {
        query = `
SELECT name, description, body_json, created_at, updated_at, primary_template_name, business_name
FROM scene_templates
ORDER BY updated_at DESC, name ASC
`
    }
    rows, err := r.db.Query(query)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var out []AssetRecord
    for rows.Next() {
        var item AssetRecord
        if err := rows.Scan(&item.Name, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt, &item.TemplateName, &item.BusinessName); err != nil {
            return nil, err
        }
        out = append(out, item)
    }
    return out, rows.Err()
}

func (r *AssetsRepo) getAsset(table string, name string) (*AssetRecord, error) {
    if r == nil || r.db == nil {
        return nil, nil
    }
    query := `
SELECT name, description, body_json, created_at, updated_at, '', ''
FROM ` + table + `
WHERE name = ?
`
    if table == "profiles" {
        query = `
SELECT name, description, body_json, created_at, updated_at, primary_template_name, business_name
FROM scene_templates
WHERE name = ?
`
    }

    var item AssetRecord
    err := r.db.QueryRow(query, name).Scan(&item.Name, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt, &item.TemplateName, &item.BusinessName)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    if err != nil {
        return nil, err
    }
    return &item, nil
}

func (r *AssetsRepo) deleteAsset(table string, name string) error {
    if r == nil || r.db == nil {
        return nil
    }
    if table == "profiles" {
        table = "scene_templates"
    }
    _, err := r.db.Exec(`DELETE FROM `+table+` WHERE name = ?`, name)
    return err
}

func rewriteProfileTemplateRefs(bodyJSON string, oldName string, newName string) (string, bool, error) {
    var raw map[string]any
    if err := json.Unmarshal([]byte(bodyJSON), &raw); err != nil {
        return "", false, err
    }
    changed := false
    if strings.TrimSpace(valueString(raw["primary_template_name"])) == oldName {
        raw["primary_template_name"] = newName
        changed = true
    }
    instances, _ := raw["instances"].([]any)
    for _, item := range instances {
        instanceMap, _ := item.(map[string]any)
        if strings.TrimSpace(valueString(instanceMap["template"])) == oldName {
            instanceMap["template"] = newName
            changed = true
        }
    }
    if !changed {
        return bodyJSON, false, nil
    }
    body, err := json.MarshalIndent(raw, "", "  ")
    if err != nil {
        return "", false, err
    }
    return string(append(body, '\n')), true, nil
}

func valueString(v any) string {
    switch vv := v.(type) {
    case string:
        return vv
    case float64:
        return fmt.Sprintf("%v", vv)
    default:
        return ""
    }
}

// ---- device_features ----

// DeviceFeatureRecord holds the persisted feature selection for a device.
type DeviceFeatureRecord struct {
    DeviceID     string `json:"device_id"`
    FeaturesJSON string `json:"features_json"` // JSON array of feature keys, e.g. ["face","shoe"]
    UpdatedAt    string `json:"updated_at"`
}

// SaveDeviceFeatures upserts the feature selection for a device.
func (r *AssetsRepo) SaveDeviceFeatures(deviceID string, featuresJSON string) error {
    if r == nil || r.db == nil {
        fmt.Printf("[DB] SaveDeviceFeatures SKIP nil\n")
        return nil
    }
    fmt.Printf("[DB] SaveDeviceFeatures device=%s val=%s\n", deviceID, featuresJSON)
    now := time.Now().Format(time.RFC3339)
    _, err := r.db.Exec(`
INSERT INTO device_features(device_id, features_json, updated_at)
VALUES(?, ?, ?)
ON CONFLICT(device_id) DO UPDATE SET
  features_json=excluded.features_json,
  updated_at=excluded.updated_at
`, deviceID, featuresJSON, now)
    return err
}

// GetDeviceFeatures returns the persisted features for a device, or nil.
func (r *AssetsRepo) GetDeviceFeatures(deviceID string) (*DeviceFeatureRecord, error) {
    if r == nil || r.db == nil {
        return nil, nil
    }
    var rec DeviceFeatureRecord
    err := r.db.QueryRow(
        "SELECT device_id, features_json, updated_at FROM device_features WHERE device_id = ?",
        deviceID,
    ).Scan(&rec.DeviceID, &rec.FeaturesJSON, &rec.UpdatedAt)
    if err != nil {
        return nil, nil // not found is not an error
    }
    return &rec, nil
}
