package cli

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"

	"github.com/google/uuid"
	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/serpent"
)

func (r *RootCmd) organizationSettings(orgContext *OrganizationContext) *serpent.Command {
	settings := []organizationSetting{
		{
			Name:    "group-sync",
			Aliases: []string{"groupsync"},
			Short:   "Group sync settings to sync groups from an IdP.",
			Patch: func(ctx context.Context, cli *codersdk.Client, org uuid.UUID, input json.RawMessage) (any, error) {
				var req codersdk.GroupSyncSettings
				err := json.Unmarshal(input, &req)
				if err != nil {
					return nil, xerrors.Errorf("unmarshalling group sync settings: %w", err)
				}
				return cli.PatchGroupIDPSyncSettings(ctx, org.String(), req)
			},
			Fetch: func(ctx context.Context, cli *codersdk.Client, org uuid.UUID) (any, error) {
				return cli.GroupIDPSyncSettings(ctx, org.String())
			},
		},
		{
			Name:    "role-sync",
			Aliases: []string{"rolesync"},
			Short:   "Role sync settings to sync organization roles from an IdP.",
			Patch: func(ctx context.Context, cli *codersdk.Client, org uuid.UUID, input json.RawMessage) (any, error) {
				var req codersdk.RoleSyncSettings
				err := json.Unmarshal(input, &req)
				if err != nil {
					return nil, xerrors.Errorf("unmarshalling role sync settings: %w", err)
				}
				return cli.PatchRoleIDPSyncSettings(ctx, org.String(), req)
			},
			Fetch: func(ctx context.Context, cli *codersdk.Client, org uuid.UUID) (any, error) {
				return cli.RoleIDPSyncSettings(ctx, org.String())
			},
		},
	}
	cmd := &serpent.Command{
		Use:     "settings",
		Short:   "Manage organization settings.",
		Aliases: []string{"setting"},
		Handler: func(inv *serpent.Invocation) error {
			return inv.Command.HelpHandler(inv)
		},
		Children: []*serpent.Command{
			r.printOrganizationSetting(orgContext, settings),
			r.setOrganizationSettings(orgContext, settings),
		},
	}
	return cmd
}

type organizationSetting struct {
	Name    string
	Aliases []string
	Short   string
	Patch   func(ctx context.Context, cli *codersdk.Client, org uuid.UUID, input json.RawMessage) (any, error)
	Fetch   func(ctx context.Context, cli *codersdk.Client, org uuid.UUID) (any, error)
}

func (r *RootCmd) setOrganizationSettings(orgContext *OrganizationContext, settings []organizationSetting) *serpent.Command {
	client := new(codersdk.Client)
	cmd := &serpent.Command{
		Use:   "set",
		Short: "Update specified organization setting.",
		Long: FormatExamples(
			Example{
				Description: "Update group sync settings.",
				Command:     "coder organization settings set groupsync < input.json",
			},
		),
		Options: []serpent.Option{},
		Middleware: serpent.Chain(
			serpent.RequireNArgs(0),
			r.InitClient(client),
		),
		Handler: func(inv *serpent.Invocation) error {
			return inv.Command.HelpHandler(inv)
		},
	}

	for _, set := range settings {
		set := set
		patch := set.Patch
		cmd.Children = append(cmd.Children, &serpent.Command{
			Use:     set.Name,
			Aliases: set.Aliases,
			Short:   set.Short,
			Options: []serpent.Option{},
			Middleware: serpent.Chain(
				serpent.RequireNArgs(0),
				r.InitClient(client),
			),
			Handler: func(inv *serpent.Invocation) error {
				ctx := inv.Context()
				org, err := orgContext.Selected(inv, client)
				if err != nil {
					return err
				}

				// Read in the json
				inputData, err := io.ReadAll(inv.Stdin)
				if err != nil {
					return xerrors.Errorf("reading stdin: %w", err)
				}

				output, err := patch(ctx, client, org.ID, inputData)
				if err != nil {
					return xerrors.Errorf("patching %q: %w", set.Name, err)
				}

				settingJSON, err := json.Marshal(output)
				if err != nil {
					return fmt.Errorf("failed to marshal organization setting %s: %w", inv.Args[0], err)
				}

				var dst bytes.Buffer
				err = json.Indent(&dst, settingJSON, "", "\t")
				if err != nil {
					return fmt.Errorf("failed to indent organization setting as json %s: %w", inv.Args[0], err)
				}

				_, err = fmt.Fprintln(inv.Stdout, dst.String())
				return err
			},
		})
	}

	return cmd
}

func (r *RootCmd) printOrganizationSetting(orgContext *OrganizationContext, settings []organizationSetting) *serpent.Command {
	client := new(codersdk.Client)
	cmd := &serpent.Command{
		Use:   "show",
		Short: "Outputs specified organization setting.",
		Long: FormatExamples(
			Example{
				Description: "Output group sync settings.",
				Command:     "coder organization settings show groupsync",
			},
		),
		Options: []serpent.Option{},
		Middleware: serpent.Chain(
			serpent.RequireNArgs(0),
			r.InitClient(client),
		),
		Handler: func(inv *serpent.Invocation) error {
			return inv.Command.HelpHandler(inv)
		},
	}

	for _, set := range settings {
		set := set
		fetch := set.Fetch
		cmd.Children = append(cmd.Children, &serpent.Command{
			Use:     set.Name,
			Aliases: set.Aliases,
			Short:   set.Short,
			Options: []serpent.Option{},
			Middleware: serpent.Chain(
				serpent.RequireNArgs(0),
				r.InitClient(client),
			),
			Handler: func(inv *serpent.Invocation) error {
				ctx := inv.Context()
				org, err := orgContext.Selected(inv, client)
				if err != nil {
					return err
				}

				output, err := fetch(ctx, client, org.ID)
				if err != nil {
					return xerrors.Errorf("patching %q: %w", set.Name, err)
				}

				settingJSON, err := json.Marshal(output)
				if err != nil {
					return fmt.Errorf("failed to marshal organization setting %s: %w", inv.Args[0], err)
				}

				var dst bytes.Buffer
				err = json.Indent(&dst, settingJSON, "", "\t")
				if err != nil {
					return fmt.Errorf("failed to indent organization setting as json %s: %w", inv.Args[0], err)
				}

				_, err = fmt.Fprintln(inv.Stdout, dst.String())
				return err
			},
		})
	}

	return cmd
}
