package core

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	_ "github.com/mattn/go-sqlite3"
)

func AddItems(db DB, items []Item) error {
	return db.Transact(func(tx DB) error {
		stmt, err := tx.Prepare(`
			insert into items (source, id, active, title, author, body, link, time, ttl, ttd, tts, action)
			values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, jsonb(?))
		`)
		if err != nil {
			return fmt.Errorf("failed to prepare insert: %v", err)
		}
		for _, item := range items {
			actions, err := json.Marshal(item.Action)
			if err != nil {
				return fmt.Errorf("failed to marshal actions for %s/%s: %v", item.Source, item.Id, err)
			}
			_, err = stmt.Exec(
				item.Source, item.Id, true, item.Title, item.Author, item.Body, item.Link, item.Time, item.Ttl, item.Ttd, item.Tts, actions,
			)
			if err != nil {
				return fmt.Errorf("failed to insert %s/%s: %v", item.Source, item.Id, err)
			}
		}
		return nil
	})
}

// Set fields in the new item to match the old item where the new item's fields are zero-valued.
// This allows sources to omit fields and let an action set them without a later fetch overwriting
// the value from the action, e.g. an on-create action archiving a page and setting the link to
// point to the archive.
func BackfillItem(new *Item, old *Item) {
	new.Active = old.Active
	new.Created = old.Created
	if new.Author == "" {
		new.Author = old.Author
	}
	if new.Body == "" {
		new.Body = old.Body
	}
	if new.Link == "" {
		new.Link = old.Link
	}
	if new.Time == 0 {
		new.Time = old.Time
	}
	if new.Title == "" {
		new.Title = old.Title
	}
	if new.Ttl == 0 {
		new.Ttl = old.Ttl
	}
	if new.Ttd == 0 {
		new.Ttd = old.Ttd
	}
	if new.Tts == 0 {
		new.Tts = old.Tts
	}
}

func UpdateItems(db DB, items []Item) error {
	return db.Transact(func(tx DB) error {
		stmt, err := tx.Prepare(`
			update items
			set
				title = ?,
				author = ?,
				body = ?,
				link = ?,
				time = ?,
				ttl = ?,
				ttd = ?,
				tts = ?,
				action = jsonb(?)
			where source = ?
			and id = ?
		`)
		if err != nil {
			return err
		}
		for _, item := range items {
			actions, err := json.Marshal(item.Action)
			if err != nil {
				return fmt.Errorf("failed to marshal actions for %s/%s: %v", item.Source, item.Id, err)
			}
			_, err = stmt.Exec(
				item.Title, item.Author, item.Body, item.Link, item.Time, item.Ttl, item.Ttd, item.Tts, actions, item.Source, item.Id,
			)
			if err != nil {
				return err
			}
		}
		return nil
	})
}

// Deactivate an item, returning its previous active state.
func DeactivateItem(db DB, source string, id string) (bool, error) {
	row := db.QueryRow(`
		select active
		from items
		where source = ? and id = ?
	`, source, id)
	var active bool
	err := row.Scan(&active)
	if err != nil && errors.Is(err, sql.ErrNoRows) {
		return false, fmt.Errorf("item %s/%s not found", source, id)
	}

	_, err = db.Exec(`
		update items
		set active = 0
		where source = ? and id = ?
	`, source, id)
	if err != nil {
		return false, err
	}
	return active, nil
}

func DeleteItem(db DB, source string, id string) (int64, error) {
	res, err := db.Exec(`
		delete from items
		where source = ?
		and id = ?
	`, source, id)
	if err != nil {
		return 0, err
	}
	return res.RowsAffected()
}

func getItems(db DB, query string, args ...any) ([]Item, error) {
	rows, err := db.Query(query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Item
	for rows.Next() {
		var item Item
		err = rows.Scan(
			&item.Source,
			&item.Id,
			&item.Created,
			&item.Active,
			&item.Title,
			&item.Author,
			&item.Body,
			&item.Link,
			&item.Time,
			&item.Ttl,
			&item.Ttd,
			&item.Tts,
			&item.Action,
		)
		if err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

func GetItem(db DB, source string, id string) (Item, error) {
	items, err := getItems(db, `
		select source, id, created, active, title, author, body, link, time, ttl, ttd, tts, json(action)
		from items
		where source = ?
		and id = ?
		order by case when time = 0 then created else time end, id
	`, source, id)
	if err != nil {
		return Item{}, err
	}
	if len(items) == 0 {
		return Item{}, fmt.Errorf("no item in %s with id %s", source, id)
	}
	return items[0], nil
}

var DefaultFeedLimit = 100

func GetAllActiveItems(db DB, offset int, limit int) ([]Item, error) {
	now := int(time.Now().Unix()) // TODO pass this value in
	return getItems(db, `
		select
			source, id, created, active, title, author, body, link, time, ttl, ttd, tts, json(action)
		from items
		where active <> 0
		and (tts = 0 or created + tts < ?)
		order by case when time = 0 then created else time end, id
		limit ? offset ?
	`, now, limit, offset)
}

func GetAllItems(db DB, offset int, limit int) ([]Item, error) {
	return getItems(db, `
		select
			source, id, created, active, title, author, body, link, time, ttl, ttd, tts, json(action)
		from items
		order by case when time = 0 then created else time end, id
		limit ? offset ?
	`, limit, offset)
}

func GetActiveItemsForSource(db DB, source string, offset int, limit int) ([]Item, error) {
	now := int(time.Now().Unix()) // TODO pass this value in
	return getItems(db, `
		select
			source, id, created, active, title, author, body, link, time, ttl, ttd, tts, json(action)
		from items
		where
			source = ?
		and active <> 0
		and (tts = 0 or created + tts < ?)
		order by case when time = 0 then created else time end, id
		limit ? offset ?
	`, source, now, limit, offset)
}

func GetAllItemsForSource(db DB, source string, offset int, limit int) ([]Item, error) {
	return getItems(db, `
		select
			source, id, created, active, title, author, body, link, time, ttl, ttd, tts, json(action)
		from items
		where
			source = ?
		order by case when time = 0 then created else time end, id
		limit ? offset ?
	`, source, limit, offset)
}

func GetActiveItemsForChannel(db DB, channel string, offset int, limit int) ([]Item, error) {
	now := int(time.Now().Unix()) // TODO pass this value in
	return getItems(db, `
		select
			i.source, i.id, i.created, i.active, i.title, i.author, i.body, i.link, i.time, i.ttl, i.ttd, i.tts, json(i.action)
		from items i
		join channels c on i.source = c.source
		where
			c.name = ?
		and i.active <> 0
		and (i.tts = 0 or i.created + i.tts < ?)
		order by case when i.time = 0 then i.created else i.time end, i.id
		limit ? offset ?
	`, channel, now, limit, offset)
}

func GetAllItemsForChannel(db DB, channel string, offset int, limit int) ([]Item, error) {
	return getItems(db, `
		select
			i.source, i.id, i.created, i.active, i.title, i.author, i.body, i.link, i.time, i.ttl, i.ttd, i.tts, json(i.action)
		from items i
		join channels c on i.source = c.source
		where
			c.name = ?
		order by case when i.time = 0 then i.created else i.time end, i.id
		limit ? offset ?
	`, channel, limit, offset)
}