package network

import (
	"context"
	"errors"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/jackc/pgx/v5/pgxpool"
	"go.jetify.com/typeid"
)

// dbtx is the subset of pgx's query API shared by *pgxpool.Pool and pgx.Tx,
// so repository helpers can run standalone or inside a transaction.
type dbtx interface {
	Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
	Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}

var errNotFound = errors.New("not found")

type networkIDPrefix struct{}

func (networkIDPrefix) Prefix() string { return "net" }

type networkID struct {
	typeid.TypeID[networkIDPrefix]
}

func newNetworkID() (networkID, error) {
	return typeid.New[networkID]()
}

type repository interface {
	create(ctx context.Context, name, adminHumanId string) (*Network, error)
	getByID(ctx context.Context, id string) (*Network, error)
	updateName(ctx context.Context, id, name string) error
	delete(ctx context.Context, id string) error
	addMember(ctx context.Context, db dbtx, networkID, humanId string) error
	removeMember(ctx context.Context, db dbtx, networkID, humanId string) error
	getMemberHumanIds(ctx context.Context, networkID string) ([]string, error)
	countSeats(ctx context.Context, db dbtx, networkID string) (int, error)
	getNetworksForHuman(ctx context.Context, humanId string) ([]*Network, error)
	isMember(ctx context.Context, networkID, humanId string) (bool, error)
	listAll(ctx context.Context) ([]*Network, error)

	// listAllMemberships returns humanId -> networkIds for every human with at
	// least one membership. Humans with zero memberships are absent from the map.
	listAllMemberships(ctx context.Context) (map[string][]string, error)

	// Invitations
	createInvitation(ctx context.Context, networkID, email string) error
	getInvitationsByEmail(ctx context.Context, email string) ([]*Invitation, error)
	getInvitationsByNetwork(ctx context.Context, networkID string) ([]*Invitation, error)
	deleteInvitation(ctx context.Context, db dbtx, networkID, email string) error
}

// Centralized so SELECTs and scanNetwork stay in sync.
const networkColumns = `id, name, admin_human_id, created_at`

func scanNetwork(row pgx.Row, n *Network) error {
	return row.Scan(&n.ID, &n.Name, &n.AdminHumanId, &n.CreatedAt)
}

type repositoryImpl struct {
	pool *pgxpool.Pool
}

func newRepository(pool *pgxpool.Pool) repository {
	return &repositoryImpl{pool: pool}
}

func (r *repositoryImpl) create(ctx context.Context, name, adminHumanId string) (*Network, error) {
	id, err := newNetworkID()
	if err != nil {
		return nil, err
	}

	var n Network
	row := r.pool.QueryRow(ctx,
		`INSERT INTO networks (id, name, admin_human_id) VALUES ($1, $2, $3)
		 RETURNING `+networkColumns,
		id.String(), name, adminHumanId,
	)
	if err := scanNetwork(row, &n); err != nil {
		return nil, err
	}

	n.MemberHumanIds = []string{}
	return &n, nil
}

func (r *repositoryImpl) getByID(ctx context.Context, id string) (*Network, error) {
	var n Network
	row := r.pool.QueryRow(ctx,
		`SELECT `+networkColumns+` FROM networks WHERE id = $1`,
		id,
	)
	if err := scanNetwork(row, &n); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errNotFound
		}
		return nil, err
	}

	memberIds, err := r.getMemberHumanIds(ctx, id)
	if err != nil {
		return nil, err
	}
	n.MemberHumanIds = memberIds

	return &n, nil
}

func (r *repositoryImpl) updateName(ctx context.Context, id, name string) error {
	result, err := r.pool.Exec(ctx,
		`UPDATE networks SET name = $1 WHERE id = $2`,
		name, id,
	)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return errNotFound
	}
	return nil
}

func (r *repositoryImpl) delete(ctx context.Context, id string) error {
	result, err := r.pool.Exec(ctx, `DELETE FROM networks WHERE id = $1`, id)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return errNotFound
	}
	return nil
}

func (r *repositoryImpl) addMember(ctx context.Context, db dbtx, networkID, humanId string) error {
	_, err := db.Exec(ctx,
		`INSERT INTO network_members (network_id, human_id) VALUES ($1, $2)
		 ON CONFLICT (network_id, human_id) DO NOTHING`,
		networkID, humanId,
	)
	return err
}

func (r *repositoryImpl) removeMember(ctx context.Context, db dbtx, networkID, humanId string) error {
	_, err := db.Exec(ctx,
		`DELETE FROM network_members WHERE network_id = $1 AND human_id = $2`,
		networkID, humanId,
	)
	return err
}

func (r *repositoryImpl) countSeats(ctx context.Context, db dbtx, networkID string) (int, error) {
	var count int
	err := db.QueryRow(ctx,
		`SELECT COUNT(*) FROM network_members WHERE network_id = $1`,
		networkID,
	).Scan(&count)
	return count, err
}

func (r *repositoryImpl) getMemberHumanIds(ctx context.Context, networkID string) ([]string, error) {
	rows, err := r.pool.Query(ctx,
		`SELECT human_id FROM network_members WHERE network_id = $1`,
		networkID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var humanIds []string
	for rows.Next() {
		var humanId string
		if err := rows.Scan(&humanId); err != nil {
			return nil, err
		}
		humanIds = append(humanIds, humanId)
	}
	return humanIds, rows.Err()
}

func (r *repositoryImpl) getNetworksForHuman(ctx context.Context, humanId string) ([]*Network, error) {
	rows, err := r.pool.Query(ctx,
		`SELECT `+networkColumns+`
		 FROM networks
		 WHERE admin_human_id = $1
		 OR EXISTS (SELECT 1 FROM network_members nm WHERE nm.network_id = id AND nm.human_id = $1)`,
		humanId,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var networks []*Network
	for rows.Next() {
		var n Network
		if err := scanNetwork(rows, &n); err != nil {
			return nil, err
		}
		networks = append(networks, &n)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	for _, n := range networks {
		n.MemberHumanIds, err = r.getMemberHumanIds(ctx, n.ID)
		if err != nil {
			return nil, err
		}
	}

	return networks, nil
}

func (r *repositoryImpl) isMember(ctx context.Context, networkID, humanId string) (bool, error) {
	var isMember bool
	err := r.pool.QueryRow(ctx, `
		SELECT EXISTS(
			SELECT 1 FROM networks n
			LEFT JOIN network_members nm ON nm.network_id = n.id AND nm.human_id = $2
			WHERE n.id = $1 AND (n.admin_human_id = $2 OR nm.human_id IS NOT NULL)
		)
	`, networkID, humanId).Scan(&isMember)
	return isMember, err
}

func (r *repositoryImpl) listAllMemberships(ctx context.Context) (map[string][]string, error) {
	rows, err := r.pool.Query(ctx, `SELECT human_id, network_id FROM network_members`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	out := map[string][]string{}
	for rows.Next() {
		var humanId, networkId string
		if err := rows.Scan(&humanId, &networkId); err != nil {
			return nil, err
		}
		out[humanId] = append(out[humanId], networkId)
	}
	return out, rows.Err()
}

func (r *repositoryImpl) listAll(ctx context.Context) ([]*Network, error) {
	rows, err := r.pool.Query(ctx,
		`SELECT `+networkColumns+` FROM networks`,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var networks []*Network
	for rows.Next() {
		var n Network
		if err := scanNetwork(rows, &n); err != nil {
			return nil, err
		}
		networks = append(networks, &n)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	for _, n := range networks {
		n.MemberHumanIds, err = r.getMemberHumanIds(ctx, n.ID)
		if err != nil {
			return nil, err
		}
	}

	return networks, nil
}

func (r *repositoryImpl) createInvitation(ctx context.Context, networkID, email string) error {
	_, err := r.pool.Exec(ctx,
		`INSERT INTO network_invitations (network_id, email) VALUES ($1, $2)
		 ON CONFLICT (network_id, email) DO NOTHING`,
		networkID, email,
	)
	return err
}

func (r *repositoryImpl) getInvitationsByEmail(ctx context.Context, email string) ([]*Invitation, error) {
	rows, err := r.pool.Query(ctx,
		`SELECT ni.network_id, n.name, ni.email, ni.created_at
		 FROM network_invitations ni
		 JOIN networks n ON n.id = ni.network_id
		 WHERE ni.email = $1`,
		email,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var invitations []*Invitation
	for rows.Next() {
		var inv Invitation
		if err := rows.Scan(&inv.NetworkID, &inv.NetworkName, &inv.Email, &inv.CreatedAt); err != nil {
			return nil, err
		}
		invitations = append(invitations, &inv)
	}
	return invitations, rows.Err()
}

func (r *repositoryImpl) getInvitationsByNetwork(ctx context.Context, networkID string) ([]*Invitation, error) {
	rows, err := r.pool.Query(ctx,
		`SELECT ni.network_id, n.name, ni.email, ni.created_at
		 FROM network_invitations ni
		 JOIN networks n ON n.id = ni.network_id
		 WHERE ni.network_id = $1`,
		networkID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var invitations []*Invitation
	for rows.Next() {
		var inv Invitation
		if err := rows.Scan(&inv.NetworkID, &inv.NetworkName, &inv.Email, &inv.CreatedAt); err != nil {
			return nil, err
		}
		invitations = append(invitations, &inv)
	}
	return invitations, rows.Err()
}

func (r *repositoryImpl) deleteInvitation(ctx context.Context, db dbtx, networkID, email string) error {
	_, err := db.Exec(ctx,
		`DELETE FROM network_invitations WHERE network_id = $1 AND email = $2`,
		networkID, email,
	)
	return err
}
