// This file is part of tdir, the Taler Directory implementation.
// Copyright (C) 2025 Martin Schanzenbach
//
// Taldir 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.
//
// Taldir 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/>.
//
// SPDX-License-Identifier: AGPL3.0-or-later

package taldir

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"time"
)

// Validation is the object created when a registration for an entry is initiated.
// The Validation stores the identity key (sha256(identity)) the secret
// Validation reference. The Validation reference is sent to the identity
// depending on the out-of-band channel defined through the identity key type.
type Validation struct {
	// When was this entry created in microseconds
	CreatedAt int64 `json:"-"`

	// The hash (SHA512) of the alias
	HAlias string `json:"h_alias"`

	// For how long should the registration last
	Duration int64 `json:"duration"`

	// Target URI to associate with this alias
	TargetURI string `json:"target_uri"`

	// The activation code sent to the client
	Challenge string `json:"-"`

	// The challenge has been sent already
	ChallengeSent bool `json:"-"`

	// true if this validation also requires payment
	RequiresPayment bool `json:"-"`

	// How often was a solution for this validation tried
	SolutionAttemptCount int

	// The beginning of the last solution timeframe
	LastSolutionTimeframeStart int64

	// The order ID associated with this validation
	OrderID string `json:"-"`

	// Name of the validator
	ValidatorName string
}

// Entry is a mapping from the alias hash to a target URI
type Entry struct {
	// When was this entry created in microseconds
	CreatedAt int64 `json:"-"`

	// The salted hash (SHA512) of the hashed alias
	HsAlias string `json:"-"`

	// Target URI to associate with this alias
	TargetURI string `json:"target_uri"`

	// How long the registration lasts in microseconds
	Duration int64 `json:"-"`
}

// Update validation in database
func UpdateValidationInDatabase(db *sql.DB, v *Validation) error {
	query := `UPDATE taler_directory.validations
						SET
								"created_at" = $2,
								"duration" = $3,
								"target_uri" = $4,
								"challenge" = $5,
								"challenge_sent" = $6,
								"requires_payment" = $7,
								"solution_attempt_count" = $8,
								"last_solution_timeframe_start" = $9,
								"order_id" = $10,
								"validator_name" = $11
						WHERE "h_alias" = $1;`
	rows, err := db.Query(query, v.HAlias, v.CreatedAt, v.Duration, v.TargetURI, v.Challenge, v.ChallengeSent, v.RequiresPayment, v.SolutionAttemptCount, v.LastSolutionTimeframeStart, v.OrderID, v.ValidatorName)
	if err != nil {
		return err
	}
	defer rows.Close()
	return nil
}

// Insert new validation into database
func InsertValidationIntoDatabase(db *sql.DB, v *Validation) error {
	query := `INSERT INTO taler_directory.validations
						VALUES (DEFAULT, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);`
	rows, err := db.Query(query, v.CreatedAt, v.HAlias, v.Duration, v.TargetURI, v.Challenge, v.ChallengeSent, v.RequiresPayment, v.SolutionAttemptCount, v.LastSolutionTimeframeStart, v.OrderID, v.ValidatorName)
	if err != nil {
		return err
	}
	defer rows.Close()
	return nil
}

// Get Validation from database
func GetValidationFromDatabase(db *sql.DB, v *Validation, hAlias string, targetURI string, duration time.Duration) error {
	query := `SELECT
								"created_at",
								"h_alias",
								"duration",
								"target_uri",
								"challenge",
								"challenge_sent",
								"requires_payment",
								"solution_attempt_count",
								"last_solution_timeframe_start",
								"order_id",
								"validator_name"
				FROM taler_directory.validations
				WHERE
					"h_alias"=$1 AND
					"target_uri"=$2 AND
					"duration"=$3
				;`
	// Execute Query
	rows, err := db.Query(query, hAlias, targetURI, duration.Microseconds())
	if err != nil {
		return err
	}
	defer rows.Close()
	// Iterate over first
	if !rows.Next() {
		fmt.Printf("error val %v\n", rows.Err())
		return errors.New("Validation does not exist")
	}
	return rows.Scan(
		&v.CreatedAt,
		&v.HAlias,
		&v.Duration,
		&v.TargetURI,
		&v.Challenge,
		&v.ChallengeSent,
		&v.RequiresPayment,
		&v.SolutionAttemptCount,
		&v.LastSolutionTimeframeStart,
		&v.OrderID,
		&v.ValidatorName,
	)
}

// Get all Validations by hash-salted alias from database
func GetAllValidationsByHAliasFromDatabase(db *sql.DB, hAlias string) ([]Validation, error) {
	query := `SELECT
								"created_at",
								"h_alias",
								"duration",
								"target_uri",
								"challenge",
								"challenge_sent",
								"requires_payment",
								"solution_attempt_count",
								"last_solution_timeframe_start",
								"order_id",
								"validator_name"
				FROM taler_directory.validations
				WHERE
					"h_alias" = $1
				;`
	// Execute Query
	rows, err := db.Query(query, hAlias)
	if err != nil {
		return []Validation{}, err
	}
	defer rows.Close()
	var validations = make([]Validation, 0)
	// Iterate over first
	for rows.Next() {
		var v Validation
		err = rows.Scan(
			&v.CreatedAt,
			&v.HAlias,
			&v.Duration,
			&v.TargetURI,
			&v.Challenge,
			&v.ChallengeSent,
			&v.RequiresPayment,
			&v.SolutionAttemptCount,
			&v.LastSolutionTimeframeStart,
			&v.OrderID,
			&v.ValidatorName,
		)
		if err != nil {
			return validations, err
		}
		validations = append(validations, v)
	}
	return validations, nil
}

// Get Hash-salted alias from database
func GetFirstValidationByHAliasFromDatabase(db *sql.DB, v *Validation, hAlias string) error {
	query := `SELECT
								"created_at",
								"h_alias",
								"duration",
								"target_uri",
								"challenge",
								"challenge_sent",
								"requires_payment",
								"solution_attempt_count",
								"last_solution_timeframe_start",
								"order_id",
								"validator_name"
				FROM taler_directory.validations
				WHERE
					"h_alias"=$1
				;`
	// Execute Query
	rows, err := db.Query(query, hAlias)
	if err != nil {
		return err
	}
	defer rows.Close()
	// Iterate over first
	if !rows.Next() {
		return errors.New("Validation not found")
	}
	return rows.Scan(
		&v.CreatedAt,
		&v.HAlias,
		&v.Duration,
		&v.TargetURI,
		&v.Challenge,
		&v.ChallengeSent,
		&v.RequiresPayment,
		&v.SolutionAttemptCount,
		&v.LastSolutionTimeframeStart,
		&v.OrderID,
		&v.ValidatorName,
	)
}

// Get Hash-salted alias from database
func GetAllEntriesFromDatabase(db *sql.DB) ([]Entry, error) {
	query := `SELECT
	                "hs_alias",
									"created_at",
									"target_uri",
									"duration"
				FROM taler_directory.entries
				WHERE
					"1" = "1"
				;`
	// Execute Query
	rows, err := db.Query(query)
	if err != nil {
		return []Entry{}, err
	}
	defer rows.Close()
	var entries = make([]Entry, 0)
	for rows.Next() {
		var e Entry
		err = rows.Scan(
			&e.HsAlias,
			&e.CreatedAt,
			&e.TargetURI,
			&e.Duration,
		)
		if err != nil {
			return entries, err
		}
		entries = append(entries, e)
	}
	return entries, nil
}

// Get Hash-salted alias from database
func GetEntryByHsAliasFromDatabase(db *sql.DB, e *Entry, hsAlias string) error {
	query := `SELECT
	                "hs_alias",
									"created_at",
									"target_uri",
									"duration"
				FROM taler_directory.entries
				WHERE
					"hs_alias"=$1
				;`
	// Execute Query
	rows, err := db.Query(query, hsAlias)
	if err != nil {
		return err
	}
	defer rows.Close()
	// Iterate over first
	if !rows.Next() {
		return errors.New("Entry not found")
	}
	return rows.Scan(
		&e.HsAlias,
		&e.CreatedAt,
		&e.TargetURI,
		&e.Duration,
	)
}

// DeleteStaleValidationsFromDatabase purges stale validations
func DeleteStaleValidationsFromDatabase(db *sql.DB, validationExpiration time.Duration) (int64, error) {
	var ctx context.Context
	ctx, stop := context.WithCancel(context.Background())
	defer stop()
	conn, err := db.Conn(ctx)
	if err != nil {
		return 0, err
	}
	defer conn.Close()
	query := `DELETE
	FROM taler_directory.validations
				WHERE
					"created_at" < $1
				;`
	// Execute Query
	cutoffTime := time.Now().Add(-validationExpiration)
	result, err := conn.ExecContext(ctx, query, cutoffTime.UnixMicro())
	if err != nil {
		return 0, err
	}
	rows, err := result.RowsAffected()
	if err != nil {
		return 0, err
	}
	return rows, nil
}

func ClearDatabase(db *sql.DB) error {
	_, err := DeleteAllEntriesFromDatabase(db)
	if err != nil {
		return err
	}
	_, err = DeleteAllValidationsFromDatabase(db)
	return err
}

// DeleteValidationsByHAliasFromDatabase purges Validations
func DeleteValidationsByHAliasFromDatabase(db *sql.DB, hAlias string) (int64, error) {
	var ctx context.Context
	ctx, stop := context.WithCancel(context.Background())
	defer stop()
	conn, err := db.Conn(ctx)
	if err != nil {
		return 0, err
	}
	defer conn.Close()
	query := `DELETE
				FROM taler_directory.validations
				WHERE
					"h_alias" = $1
				;`
	// Execute Query
	result, err := conn.ExecContext(ctx, query, hAlias)
	if err != nil {
		return 0, err
	}
	rows, err := result.RowsAffected()
	if err != nil {
		return 0, err
	}
	return rows, nil
}

// Purge Validations
func DeleteAllValidationsFromDatabase(db *sql.DB) (int64, error) {
	var ctx context.Context
	ctx, stop := context.WithCancel(context.Background())
	defer stop()
	conn, err := db.Conn(ctx)
	if err != nil {
		return 0, err
	}
	defer conn.Close()
	query := `DELETE
				FROM taler_directory.validations
				WHERE
					1 = 1
				;`
	// Execute Query
	result, err := conn.ExecContext(ctx, query)
	if err != nil {
		return 0, err
	}
	rows, err := result.RowsAffected()
	if err != nil {
		return 0, err
	}
	return rows, nil
}

// Purge Validations
func DeleteValidationFromDatabase(db *sql.DB, v *Validation) (int64, error) {
	return DeleteValidationsByHAliasFromDatabase(db, v.HAlias)
}

// Purge Entries
func DeleteAllEntriesFromDatabase(db *sql.DB) (int64, error) {
	var ctx context.Context
	ctx, stop := context.WithCancel(context.Background())
	defer stop()
	conn, err := db.Conn(ctx)
	if err != nil {
		return 0, err
	}
	defer conn.Close()
	query := `DELETE
				FROM taler_directory.entries
				WHERE
					1=1
				;`
	// Execute Query
	result, err := conn.ExecContext(ctx, query)
	if err != nil {
		return 0, err
	}
	rows, err := result.RowsAffected()
	if err != nil {
		return 0, err
	}
	return rows, nil
}

// Delete Entry
func DeleteEntryFromDatabase(db *sql.DB, e *Entry) (int64, error) {
	var ctx context.Context
	ctx, stop := context.WithCancel(context.Background())
	defer stop()
	conn, err := db.Conn(ctx)
	if err != nil {
		return 0, err
	}
	defer conn.Close()
	query := `DELETE
				FROM taler_directory.entries
				WHERE
					"hs_alias" = $1
				;`
	// Execute Query
	result, err := conn.ExecContext(ctx, query, e.HsAlias)
	if err != nil {
		return 0, err
	}
	rows, err := result.RowsAffected()
	if err != nil {
		return 0, err
	}
	return rows, nil
}

// Update Entry in database
func UpdateEntryInDatabase(db *sql.DB, e *Entry) error {
	query := `UPDATE taler_directory.entries
						SET
								"created_at" = $2,
								"target_uri" = $3,
								"duration" = $4
						WHERE "hs_alias" = $1;`
	rows, err := db.Query(query, e.HsAlias, e.CreatedAt, e.TargetURI, e.Duration)
	if err != nil {
		return err
	}
	defer rows.Close()
	return nil
}

// InsertEntryIntoDatabase inserts new Entry into database
func InsertEntryIntoDatabase(db *sql.DB, e *Entry) error {
	e.CreatedAt = time.Now().UnixMicro()
	query := `INSERT INTO taler_directory.entries
						VALUES (DEFAULT, $1, $2, $3, $4);`
	rows, err := db.Query(query, e.HsAlias, e.CreatedAt, e.TargetURI, e.Duration)
	if err != nil {
		return err
	}
	defer rows.Close()
	return nil
}
