// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package migrations

import (
	"context"

	"code.superseriousbusiness.org/gopkg/log"

	newmodel "code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20240620074530_interaction_policy/new"
	oldmodel "code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20240620074530_interaction_policy/old"

	"github.com/uptrace/bun"
)

func init() {
	up := func(ctx context.Context, db *bun.DB) error {
		log.Info(ctx, "migrating statuses and account settings to interaction policy model, please wait...")
		log.Warn(ctx, "**WITH A LARGE DATABASE / LOWER SPEC MACHINE, THIS MIGRATION MAY TAKE A VERY LONG TIME (an hour or even longer); DO NOT INTERRUPT IT!**")
		return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {

			// Add new columns for interaction
			// policies + related fields.
			type spec struct {
				table      string
				column     string
				columnType string
				extra      string
			}
			for _, spec := range []spec{
				// Statuses.
				{
					table:      "statuses",
					column:     "interaction_policy",
					columnType: "JSONB",
					extra:      "",
				},
				{
					table:      "statuses",
					column:     "pending_approval",
					columnType: "BOOLEAN",
					extra:      "NOT NULL DEFAULT false",
				},
				{
					table:      "statuses",
					column:     "approved_by_uri",
					columnType: "varchar",
					extra:      "",
				},

				// Status faves.
				{
					table:      "status_faves",
					column:     "pending_approval",
					columnType: "BOOLEAN",
					extra:      "NOT NULL DEFAULT false",
				},
				{
					table:      "status_faves",
					column:     "approved_by_uri",
					columnType: "varchar",
					extra:      "",
				},

				// Columns that must be added to the
				// `account_settings` table to populate
				// default interaction policies for
				// different status visibilities.
				{
					table:      "account_settings",
					column:     "interaction_policy_direct",
					columnType: "JSONB",
					extra:      "",
				},
				{
					table:      "account_settings",
					column:     "interaction_policy_mutuals_only",
					columnType: "JSONB",
					extra:      "",
				},
				{
					table:      "account_settings",
					column:     "interaction_policy_followers_only",
					columnType: "JSONB",
					extra:      "",
				},
				{
					table:      "account_settings",
					column:     "interaction_policy_unlocked",
					columnType: "JSONB",
					extra:      "",
				},
				{
					table:      "account_settings",
					column:     "interaction_policy_public",
					columnType: "JSONB",
					extra:      "",
				},
			} {
				exists, err := doesColumnExist(ctx, tx,
					spec.table, spec.column,
				)
				if err != nil {
					// Real error.
					return err
				} else if exists {
					// Already created.
					continue
				}

				args := []any{
					bun.Ident(spec.table),
					bun.Ident(spec.column),
					bun.Safe(spec.columnType),
				}

				qStr := "ALTER TABLE ? ADD COLUMN ? ?"
				if spec.extra != "" {
					qStr += " ?"
					args = append(args, bun.Safe(spec.extra))
				}

				log.Infof(ctx, "adding column '%s' to '%s'...", spec.column, spec.table)
				if _, err := tx.ExecContext(ctx, qStr, args...); err != nil {
					return err
				}
			}

			// Select each locally-created status
			// with non-default old flags set.
			oldStatuses := []oldmodel.Status{}

			log.Info(ctx, "migrating existing statuses to new visibility model...")
			if err := tx.
				NewSelect().
				Model(&oldStatuses).
				Column("id", "likeable", "replyable", "boostable", "visibility").
				Where("? = ?", bun.Ident("local"), true).
				WhereGroup(" AND ", func(sq *bun.SelectQuery) *bun.SelectQuery {
					return sq.
						Where("? = ?", bun.Ident("likeable"), false).
						WhereOr("? = ?", bun.Ident("replyable"), false).
						WhereOr("? = ?", bun.Ident("boostable"), false)
				}).
				Scan(ctx); err != nil {
				return err
			}

			// For each status found in this way, update
			// to new version of interaction policy.
			for _, oldStatus := range oldStatuses {
				// Start with default policy for this visibility.
				policy := newmodel.DefaultInteractionPolicyFor(newmodel.Visibility(oldStatus.Visibility))

				if !*oldStatus.Likeable {
					// Only author can like.
					policy.CanLike = newmodel.PolicyRules{
						Always: newmodel.PolicyValues{
							newmodel.PolicyValueAuthor,
						},
						WithApproval: make(newmodel.PolicyValues, 0),
					}
				}

				if !*oldStatus.Replyable {
					// Only author + mentioned can Reply.
					policy.CanReply = newmodel.PolicyRules{
						Always: newmodel.PolicyValues{
							newmodel.PolicyValueAuthor,
							newmodel.PolicyValueMentioned,
						},
						WithApproval: make(newmodel.PolicyValues, 0),
					}
				}

				if !*oldStatus.Boostable {
					// Only author can Announce.
					policy.CanAnnounce = newmodel.PolicyRules{
						Always: newmodel.PolicyValues{
							newmodel.PolicyValueAuthor,
						},
						WithApproval: make(newmodel.PolicyValues, 0),
					}
				}

				// Update status with the new interaction policy.
				newStatus := &newmodel.Status{
					ID:                oldStatus.ID,
					InteractionPolicy: policy,
				}
				if _, err := tx.
					NewUpdate().
					Model(newStatus).
					Column("interaction_policy").
					Where("? = ?", bun.Ident("id"), newStatus.ID).
					Exec(ctx); err != nil {
					return err
				}
			}

			// Drop now unused columns from statuses table.
			oldColumns := []string{
				"likeable",
				"replyable",
				"boostable",
			}
			for _, column := range oldColumns {
				log.Infof(ctx, "dropping now-unused status column '%s'; this may take a while if you have lots of statuses in your database...", column)
				if _, err := tx.
					NewDropColumn().
					Table("statuses").
					Column(column).
					Exec(ctx); err != nil {
					return err
				}
			}

			// Add new indexes.
			log.Info(ctx, "adding new index 'statuses_pending_approval_idx' to 'statuses'...")
			if _, err := tx.
				NewCreateIndex().
				Table("statuses").
				Index("statuses_pending_approval_idx").
				Column("pending_approval").
				IfNotExists().
				Exec(ctx); err != nil {
				return err
			}

			log.Info(ctx, "adding new index 'status_faves_pending_approval_idx' to 'status_faves'...")
			if _, err := tx.
				NewCreateIndex().
				Table("status_faves").
				Index("status_faves_pending_approval_idx").
				Column("pending_approval").
				IfNotExists().
				Exec(ctx); err != nil {
				return err
			}

			log.Info(ctx, "committing transaction, almost done...")
			return nil
		})
	}

	down := func(ctx context.Context, db *bun.DB) error {
		return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
			return nil
		})
	}

	if err := Migrations.Register(up, down); err != nil {
		panic(err)
	}
}
