package coderd

import (
	"database/sql"
	"errors"
	"fmt"
	"net/http"
	"sort"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"
	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/coderd/audit"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbauthz"
	"github.com/coder/coder/v2/coderd/database/dbtime"
	"github.com/coder/coder/v2/coderd/httpapi"
	"github.com/coder/coder/v2/coderd/httpmw"
	"github.com/coder/coder/v2/coderd/rbac"
	"github.com/coder/coder/v2/coderd/schedule"
	"github.com/coder/coder/v2/coderd/telemetry"
	"github.com/coder/coder/v2/coderd/util/ptr"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/examples"
)

// Returns a single template.
//
// @Summary Get template metadata by ID
// @ID get-template-metadata-by-id
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param template path string true "Template ID" format(uuid)
// @Success 200 {object} codersdk.Template
// @Router /templates/{template} [get]
func (api *API) template(rw http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	template := httpmw.TemplateParam(r)

	httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(template))
}

// @Summary Delete template by ID
// @ID delete-template-by-id
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param template path string true "Template ID" format(uuid)
// @Success 200 {object} codersdk.Response
// @Router /templates/{template} [delete]
func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
	var (
		ctx               = r.Context()
		template          = httpmw.TemplateParam(r)
		auditor           = *api.Auditor.Load()
		aReq, commitAudit = audit.InitRequest[database.Template](rw, &audit.RequestParams{
			Audit:          auditor,
			Log:            api.Logger,
			Request:        r,
			Action:         database.AuditActionDelete,
			OrganizationID: template.OrganizationID,
		})
	)
	defer commitAudit()
	aReq.Old = template

	// This is just to get the workspace count, so we use a system context to
	// return ALL workspaces. Not just workspaces the user can view.
	// nolint:gocritic
	workspaces, err := api.Database.GetWorkspaces(dbauthz.AsSystemRestricted(ctx), database.GetWorkspacesParams{
		TemplateIDs: []uuid.UUID{template.ID},
	})
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error fetching workspaces by template id.",
			Detail:  err.Error(),
		})
		return
	}
	if len(workspaces) > 0 {
		httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
			Message: "All workspaces must be deleted before a template can be removed.",
		})
		return
	}
	err = api.Database.UpdateTemplateDeletedByID(ctx, database.UpdateTemplateDeletedByIDParams{
		ID:        template.ID,
		Deleted:   true,
		UpdatedAt: dbtime.Now(),
	})
	if err != nil {
		httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error deleting template.",
			Detail:  err.Error(),
		})
		return
	}
	httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
		Message: "Template has been deleted!",
	})
}

// Create a new template in an organization.
// Returns a single template.
//
// @Summary Create template by organization
// @ID create-template-by-organization
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Templates
// @Param request body codersdk.CreateTemplateRequest true "Request body"
// @Param organization path string true "Organization ID"
// @Success 200 {object} codersdk.Template
// @Router /organizations/{organization}/templates [post]
func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Request) {
	var (
		ctx                                = r.Context()
		createTemplate                     codersdk.CreateTemplateRequest
		organization                       = httpmw.OrganizationParam(r)
		apiKey                             = httpmw.APIKey(r)
		auditor                            = *api.Auditor.Load()
		templateAudit, commitTemplateAudit = audit.InitRequest[database.Template](rw, &audit.RequestParams{
			Audit:          auditor,
			Log:            api.Logger,
			Request:        r,
			Action:         database.AuditActionCreate,
			OrganizationID: organization.ID,
		})
		templateVersionAudit, commitTemplateVersionAudit = audit.InitRequest[database.TemplateVersion](rw, &audit.RequestParams{
			Audit:          auditor,
			Log:            api.Logger,
			Request:        r,
			Action:         database.AuditActionWrite,
			OrganizationID: organization.ID,
		})
	)
	defer commitTemplateAudit()
	defer commitTemplateVersionAudit()

	if !httpapi.Read(ctx, rw, r, &createTemplate) {
		return
	}

	// Make a temporary struct to represent the template. This is used for
	// auditing if any of the following checks fail. It will be overwritten when
	// the template is inserted into the db.
	templateAudit.New = database.Template{
		OrganizationID: organization.ID,
		Name:           createTemplate.Name,
		Description:    createTemplate.Description,
		CreatedBy:      apiKey.UserID,
		Icon:           createTemplate.Icon,
		DisplayName:    createTemplate.DisplayName,
	}

	_, err := api.Database.GetTemplateByOrganizationAndName(ctx, database.GetTemplateByOrganizationAndNameParams{
		OrganizationID: organization.ID,
		Name:           createTemplate.Name,
	})
	if err == nil {
		httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
			Message: fmt.Sprintf("Template with name %q already exists.", createTemplate.Name),
			Validations: []codersdk.ValidationError{{
				Field:  "name",
				Detail: "This value is already in use and should be unique.",
			}},
		})
		return
	}
	if !errors.Is(err, sql.ErrNoRows) {
		httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error fetching template by name.",
			Detail:  err.Error(),
		})
		return
	}

	templateVersion, err := api.Database.GetTemplateVersionByID(ctx, createTemplate.VersionID)
	if errors.Is(err, sql.ErrNoRows) {
		httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
			Message: fmt.Sprintf("Template version %q does not exist.", createTemplate.VersionID),
			Validations: []codersdk.ValidationError{
				{Field: "template_version_id", Detail: "Template version does not exist"},
			},
		})
		return
	}
	if err != nil {
		httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error fetching template version.",
			Detail:  err.Error(),
		})
		return
	}
	if templateVersion.Archived {
		httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
			Message: fmt.Sprintf("Template version %s is archived.", createTemplate.VersionID),
			Validations: []codersdk.ValidationError{
				{Field: "template_version_id", Detail: "Template version is archived"},
			},
		})
		return
	}
	templateVersionAudit.Old = templateVersion
	if templateVersion.TemplateID.Valid {
		httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
			Message: fmt.Sprintf("Template version %s is already part of a template", createTemplate.VersionID),
			Validations: []codersdk.ValidationError{
				{Field: "template_version_id", Detail: "Template version is already part of a template"},
			},
		})
		return
	}

	importJob, err := api.Database.GetProvisionerJobByID(ctx, templateVersion.JobID)
	if err != nil {
		httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error fetching provisioner job.",
			Detail:  err.Error(),
		})
		return
	}

	var (
		defaultTTL                     time.Duration
		activityBump                   = time.Hour // default
		autostopRequirementDaysOfWeek  []string
		autostartRequirementDaysOfWeek []string
		autostopRequirementWeeks       int64
		failureTTL                     time.Duration
		dormantTTL                     time.Duration
		dormantAutoDeletionTTL         time.Duration
	)
	if createTemplate.DefaultTTLMillis != nil {
		defaultTTL = time.Duration(*createTemplate.DefaultTTLMillis) * time.Millisecond
	}
	if createTemplate.ActivityBumpMillis != nil {
		activityBump = time.Duration(*createTemplate.ActivityBumpMillis) * time.Millisecond
	}
	if createTemplate.AutostopRequirement != nil {
		autostopRequirementDaysOfWeek = createTemplate.AutostopRequirement.DaysOfWeek
		autostopRequirementWeeks = createTemplate.AutostopRequirement.Weeks
	}
	if createTemplate.AutostartRequirement != nil {
		autostartRequirementDaysOfWeek = createTemplate.AutostartRequirement.DaysOfWeek
	} else {
		// By default, we want to allow all days of the week to be autostarted.
		autostartRequirementDaysOfWeek = codersdk.BitmapToWeekdays(0b01111111)
	}
	if createTemplate.FailureTTLMillis != nil {
		failureTTL = time.Duration(*createTemplate.FailureTTLMillis) * time.Millisecond
	}
	if createTemplate.TimeTilDormantMillis != nil {
		dormantTTL = time.Duration(*createTemplate.TimeTilDormantMillis) * time.Millisecond
	}
	if createTemplate.TimeTilDormantAutoDeleteMillis != nil {
		dormantAutoDeletionTTL = time.Duration(*createTemplate.TimeTilDormantAutoDeleteMillis) * time.Millisecond
	}

	var (
		validErrs                            []codersdk.ValidationError
		autostopRequirementDaysOfWeekParsed  uint8
		autostartRequirementDaysOfWeekParsed uint8
	)
	if defaultTTL < 0 {
		validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be a positive integer."})
	}
	if activityBump < 0 {
		validErrs = append(validErrs, codersdk.ValidationError{Field: "activity_bump_ms", Detail: "Must be a positive integer."})
	}

	if len(autostopRequirementDaysOfWeek) > 0 {
		autostopRequirementDaysOfWeekParsed, err = codersdk.WeekdaysToBitmap(autostopRequirementDaysOfWeek)
		if err != nil {
			validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.days_of_week", Detail: err.Error()})
		}
	}
	if len(autostartRequirementDaysOfWeek) > 0 {
		autostartRequirementDaysOfWeekParsed, err = codersdk.WeekdaysToBitmap(autostartRequirementDaysOfWeek)
		if err != nil {
			validErrs = append(validErrs, codersdk.ValidationError{Field: "autostart_requirement.days_of_week", Detail: err.Error()})
		}
	}

	if autostopRequirementWeeks < 0 {
		validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: "Must be a positive integer."})
	}
	if autostopRequirementWeeks > schedule.MaxTemplateAutostopRequirementWeeks {
		validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: fmt.Sprintf("Must be less than %d.", schedule.MaxTemplateAutostopRequirementWeeks)})
	}
	if failureTTL < 0 {
		validErrs = append(validErrs, codersdk.ValidationError{Field: "failure_ttl_ms", Detail: "Must be a positive integer."})
	}
	if dormantTTL < 0 {
		validErrs = append(validErrs, codersdk.ValidationError{Field: "time_til_dormant_autodeletion_ms", Detail: "Must be a positive integer."})
	}
	if dormantAutoDeletionTTL < 0 {
		validErrs = append(validErrs, codersdk.ValidationError{Field: "time_til_dormant_autodeletion_ms", Detail: "Must be a positive integer."})
	}

	if len(validErrs) > 0 {
		httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
			Message:     "Invalid create template request.",
			Validations: validErrs,
		})
		return
	}

	var (
		dbTemplate database.Template

		allowUserCancelWorkspaceJobs = ptr.NilToDefault(createTemplate.AllowUserCancelWorkspaceJobs, false)
		allowUserAutostart           = ptr.NilToDefault(createTemplate.AllowUserAutostart, true)
		allowUserAutostop            = ptr.NilToDefault(createTemplate.AllowUserAutostop, true)
	)

	defaultsGroups := database.TemplateACL{}
	if !createTemplate.DisableEveryoneGroupAccess {
		// The organization ID is used as the group ID for the everyone group
		// in this organization.
		defaultsGroups[organization.ID.String()] = []rbac.Action{rbac.ActionRead}
	}
	err = api.Database.InTx(func(tx database.Store) error {
		now := dbtime.Now()
		id := uuid.New()
		err = tx.InsertTemplate(ctx, database.InsertTemplateParams{
			ID:                           id,
			CreatedAt:                    now,
			UpdatedAt:                    now,
			OrganizationID:               organization.ID,
			Name:                         createTemplate.Name,
			Provisioner:                  importJob.Provisioner,
			ActiveVersionID:              templateVersion.ID,
			Description:                  createTemplate.Description,
			CreatedBy:                    apiKey.UserID,
			UserACL:                      database.TemplateACL{},
			GroupACL:                     defaultsGroups,
			DisplayName:                  createTemplate.DisplayName,
			Icon:                         createTemplate.Icon,
			AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
			MaxPortSharingLevel:          database.AppSharingLevelOwner,
		})
		if err != nil {
			return xerrors.Errorf("insert template: %s", err)
		}

		if createTemplate.RequireActiveVersion {
			err = (*api.AccessControlStore.Load()).SetTemplateAccessControl(ctx, tx, id, dbauthz.TemplateAccessControl{
				RequireActiveVersion: createTemplate.RequireActiveVersion,
			})
			if err != nil {
				return xerrors.Errorf("set template access control: %w", err)
			}
		}

		dbTemplate, err = tx.GetTemplateByID(ctx, id)
		if err != nil {
			return xerrors.Errorf("get template by id: %s", err)
		}

		dbTemplate, err = (*api.TemplateScheduleStore.Load()).Set(ctx, tx, dbTemplate, schedule.TemplateScheduleOptions{
			UserAutostartEnabled: allowUserAutostart,
			UserAutostopEnabled:  allowUserAutostop,
			DefaultTTL:           defaultTTL,
			ActivityBump:         activityBump,
			// Some of these values are enterprise-only, but the
			// TemplateScheduleStore will handle avoiding setting them if
			// unlicensed.
			AutostopRequirement: schedule.TemplateAutostopRequirement{
				DaysOfWeek: autostopRequirementDaysOfWeekParsed,
				Weeks:      autostopRequirementWeeks,
			},
			AutostartRequirement: schedule.TemplateAutostartRequirement{
				DaysOfWeek: autostartRequirementDaysOfWeekParsed,
			},
			FailureTTL:               failureTTL,
			TimeTilDormant:           dormantTTL,
			TimeTilDormantAutoDelete: dormantAutoDeletionTTL,
		})
		if err != nil {
			return xerrors.Errorf("set template schedule options: %s", err)
		}

		templateAudit.New = dbTemplate

		err = tx.UpdateTemplateVersionByID(ctx, database.UpdateTemplateVersionByIDParams{
			ID: templateVersion.ID,
			TemplateID: uuid.NullUUID{
				UUID:  dbTemplate.ID,
				Valid: true,
			},
			UpdatedAt: dbtime.Now(),
			Name:      templateVersion.Name,
			Message:   templateVersion.Message,
		})
		if err != nil {
			return xerrors.Errorf("insert template version: %s", err)
		}
		newTemplateVersion := templateVersion
		newTemplateVersion.TemplateID = uuid.NullUUID{
			UUID:  dbTemplate.ID,
			Valid: true,
		}
		templateVersionAudit.New = newTemplateVersion

		return nil
	}, nil)
	if err != nil {
		httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error inserting template.",
			Detail:  err.Error(),
		})
		return
	}

	api.Telemetry.Report(&telemetry.Snapshot{
		Templates:        []telemetry.Template{telemetry.ConvertTemplate(dbTemplate)},
		TemplateVersions: []telemetry.TemplateVersion{telemetry.ConvertTemplateVersion(templateVersion)},
	})

	httpapi.Write(ctx, rw, http.StatusCreated, api.convertTemplate(dbTemplate))
}

// @Summary Get templates by organization
// @ID get-templates-by-organization
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param organization path string true "Organization ID" format(uuid)
// @Success 200 {array} codersdk.Template
// @Router /organizations/{organization}/templates [get]
func (api *API) templatesByOrganization(rw http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	organization := httpmw.OrganizationParam(r)

	p := httpapi.NewQueryParamParser()
	values := r.URL.Query()

	deprecated := sql.NullBool{}
	if values.Has("deprecated") {
		deprecated = sql.NullBool{
			Bool:  p.Boolean(values, false, "deprecated"),
			Valid: true,
		}
	}
	if len(p.Errors) > 0 {
		httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
			Message:     "Invalid query params.",
			Validations: p.Errors,
		})
		return
	}

	prepared, err := api.HTTPAuth.AuthorizeSQLFilter(r, rbac.ActionRead, rbac.ResourceTemplate.Type)
	if err != nil {
		httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error preparing sql filter.",
			Detail:  err.Error(),
		})
		return
	}

	// Filter templates based on rbac permissions
	templates, err := api.Database.GetAuthorizedTemplates(ctx, database.GetTemplatesWithFilterParams{
		OrganizationID: organization.ID,
		Deprecated:     deprecated,
	}, prepared)
	if errors.Is(err, sql.ErrNoRows) {
		err = nil
	}

	if err != nil {
		httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error fetching templates in organization.",
			Detail:  err.Error(),
		})
		return
	}

	httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplates(templates))
}

// @Summary Get templates by organization and template name
// @ID get-templates-by-organization-and-template-name
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param organization path string true "Organization ID" format(uuid)
// @Param templatename path string true "Template name"
// @Success 200 {object} codersdk.Template
// @Router /organizations/{organization}/templates/{templatename} [get]
func (api *API) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	organization := httpmw.OrganizationParam(r)
	templateName := chi.URLParam(r, "templatename")
	template, err := api.Database.GetTemplateByOrganizationAndName(ctx, database.GetTemplateByOrganizationAndNameParams{
		OrganizationID: organization.ID,
		Name:           templateName,
	})
	if err != nil {
		if httpapi.Is404Error(err) {
			httpapi.ResourceNotFound(rw)
			return
		}

		httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error fetching template.",
			Detail:  err.Error(),
		})
		return
	}

	httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(template))
}

// @Summary Update template metadata by ID
// @ID update-template-metadata-by-id
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param template path string true "Template ID" format(uuid)
// @Success 200 {object} codersdk.Template
// @Router /templates/{template} [patch]
func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
	var (
		ctx               = r.Context()
		template          = httpmw.TemplateParam(r)
		auditor           = *api.Auditor.Load()
		portSharer        = *api.PortSharer.Load()
		aReq, commitAudit = audit.InitRequest[database.Template](rw, &audit.RequestParams{
			Audit:          auditor,
			Log:            api.Logger,
			Request:        r,
			Action:         database.AuditActionWrite,
			OrganizationID: template.OrganizationID,
		})
	)
	defer commitAudit()
	aReq.Old = template

	scheduleOpts, err := (*api.TemplateScheduleStore.Load()).Get(ctx, api.Database, template.ID)
	if err != nil {
		httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error fetching template schedule options.",
			Detail:  err.Error(),
		})
		return
	}

	var req codersdk.UpdateTemplateMeta
	if !httpapi.Read(ctx, rw, r, &req) {
		return
	}

	var (
		validErrs                            []codersdk.ValidationError
		autostopRequirementDaysOfWeekParsed  uint8
		autostartRequirementDaysOfWeekParsed uint8
	)
	if req.DefaultTTLMillis < 0 {
		validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be a positive integer."})
	}
	if req.ActivityBumpMillis < 0 {
		validErrs = append(validErrs, codersdk.ValidationError{Field: "activity_bump_ms", Detail: "Must be a positive integer."})
	}

	if req.AutostopRequirement == nil {
		req.AutostopRequirement = &codersdk.TemplateAutostopRequirement{
			DaysOfWeek: codersdk.BitmapToWeekdays(scheduleOpts.AutostopRequirement.DaysOfWeek),
			Weeks:      scheduleOpts.AutostopRequirement.Weeks,
		}
	}
	if len(req.AutostopRequirement.DaysOfWeek) > 0 {
		autostopRequirementDaysOfWeekParsed, err = codersdk.WeekdaysToBitmap(req.AutostopRequirement.DaysOfWeek)
		if err != nil {
			validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.days_of_week", Detail: err.Error()})
		}
	}
	if req.AutostartRequirement == nil {
		req.AutostartRequirement = &codersdk.TemplateAutostartRequirement{
			DaysOfWeek: codersdk.BitmapToWeekdays(scheduleOpts.AutostartRequirement.DaysOfWeek),
		}
	}
	if len(req.AutostartRequirement.DaysOfWeek) > 0 {
		autostartRequirementDaysOfWeekParsed, err = codersdk.WeekdaysToBitmap(req.AutostartRequirement.DaysOfWeek)
		if err != nil {
			validErrs = append(validErrs, codersdk.ValidationError{Field: "autostart_requirement.days_of_week", Detail: err.Error()})
		}
	}
	if req.AutostopRequirement.Weeks < 0 {
		validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: "Must be a positive integer."})
	}
	if req.AutostopRequirement.Weeks == 0 {
		req.AutostopRequirement.Weeks = 1
	}
	if template.AutostopRequirementWeeks <= 0 {
		template.AutostopRequirementWeeks = 1
	}
	if req.AutostopRequirement.Weeks > schedule.MaxTemplateAutostopRequirementWeeks {
		validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: fmt.Sprintf("Must be less than %d.", schedule.MaxTemplateAutostopRequirementWeeks)})
	}
	// Defaults to the existing.
	deprecationMessage := template.Deprecated
	if req.DeprecationMessage != nil {
		deprecationMessage = *req.DeprecationMessage
	}

	// The minimum valid value for a dormant TTL is 1 minute. This is
	// to ensure an uninformed user does not send an unintentionally
	// small number resulting in potentially catastrophic consequences.
	const minTTL = 1000 * 60
	if req.FailureTTLMillis < 0 || (req.FailureTTLMillis > 0 && req.FailureTTLMillis < minTTL) {
		validErrs = append(validErrs, codersdk.ValidationError{Field: "failure_ttl_ms", Detail: "Value must be at least one minute."})
	}
	if req.TimeTilDormantMillis < 0 || (req.TimeTilDormantMillis > 0 && req.TimeTilDormantMillis < minTTL) {
		validErrs = append(validErrs, codersdk.ValidationError{Field: "time_til_dormant_ms", Detail: "Value must be at least one minute."})
	}
	if req.TimeTilDormantAutoDeleteMillis < 0 || (req.TimeTilDormantAutoDeleteMillis > 0 && req.TimeTilDormantAutoDeleteMillis < minTTL) {
		validErrs = append(validErrs, codersdk.ValidationError{Field: "time_til_dormant_autodelete_ms", Detail: "Value must be at least one minute."})
	}
	maxPortShareLevel := template.MaxPortSharingLevel
	if req.MaxPortShareLevel != nil && *req.MaxPortShareLevel != codersdk.WorkspaceAgentPortShareLevel(maxPortShareLevel) {
		err := portSharer.ValidateTemplateMaxPortSharingLevel(*req.MaxPortShareLevel)
		if err != nil {
			validErrs = append(validErrs, codersdk.ValidationError{Field: "max_port_sharing_level", Detail: err.Error()})
		} else {
			maxPortShareLevel = database.AppSharingLevel(*req.MaxPortShareLevel)
		}
	}

	if len(validErrs) > 0 {
		httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
			Message:     "Invalid request to update template metadata!",
			Validations: validErrs,
		})
		return
	}

	var updated database.Template
	err = api.Database.InTx(func(tx database.Store) error {
		if req.Name == template.Name &&
			req.Description == template.Description &&
			req.DisplayName == template.DisplayName &&
			req.Icon == template.Icon &&
			req.AllowUserAutostart == template.AllowUserAutostart &&
			req.AllowUserAutostop == template.AllowUserAutostop &&
			req.AllowUserCancelWorkspaceJobs == template.AllowUserCancelWorkspaceJobs &&
			req.DefaultTTLMillis == time.Duration(template.DefaultTTL).Milliseconds() &&
			req.ActivityBumpMillis == time.Duration(template.ActivityBump).Milliseconds() &&
			autostopRequirementDaysOfWeekParsed == scheduleOpts.AutostopRequirement.DaysOfWeek &&
			autostartRequirementDaysOfWeekParsed == scheduleOpts.AutostartRequirement.DaysOfWeek &&
			req.AutostopRequirement.Weeks == scheduleOpts.AutostopRequirement.Weeks &&
			req.FailureTTLMillis == time.Duration(template.FailureTTL).Milliseconds() &&
			req.TimeTilDormantMillis == time.Duration(template.TimeTilDormant).Milliseconds() &&
			req.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() &&
			req.RequireActiveVersion == template.RequireActiveVersion &&
			(deprecationMessage == template.Deprecated) &&
			maxPortShareLevel == template.MaxPortSharingLevel {
			return nil
		}

		// Users should not be able to clear the template name in the UI
		name := req.Name
		if name == "" {
			name = template.Name
		}

		groupACL := template.GroupACL
		if req.DisableEveryoneGroupAccess {
			delete(groupACL, template.OrganizationID.String())
		}

		if template.MaxPortSharingLevel != maxPortShareLevel {
			switch maxPortShareLevel {
			case database.AppSharingLevelOwner:
				err = tx.DeleteWorkspaceAgentPortSharesByTemplate(ctx, template.ID)
				if err != nil {
					return xerrors.Errorf("delete workspace agent port shares by template: %w", err)
				}
			case database.AppSharingLevelAuthenticated:
				err = tx.ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx, template.ID)
				if err != nil {
					return xerrors.Errorf("reduce workspace agent share level to authenticated by template: %w", err)
				}
			}
		}

		var err error
		err = tx.UpdateTemplateMetaByID(ctx, database.UpdateTemplateMetaByIDParams{
			ID:                           template.ID,
			UpdatedAt:                    dbtime.Now(),
			Name:                         name,
			DisplayName:                  req.DisplayName,
			Description:                  req.Description,
			Icon:                         req.Icon,
			AllowUserCancelWorkspaceJobs: req.AllowUserCancelWorkspaceJobs,
			GroupACL:                     groupACL,
			MaxPortSharingLevel:          maxPortShareLevel,
		})
		if err != nil {
			return xerrors.Errorf("update template metadata: %w", err)
		}

		if template.RequireActiveVersion != req.RequireActiveVersion || deprecationMessage != template.Deprecated {
			err = (*api.AccessControlStore.Load()).SetTemplateAccessControl(ctx, tx, template.ID, dbauthz.TemplateAccessControl{
				RequireActiveVersion: req.RequireActiveVersion,
				Deprecated:           deprecationMessage,
			})
			if err != nil {
				return xerrors.Errorf("set template access control: %w", err)
			}
		}

		updated, err = tx.GetTemplateByID(ctx, template.ID)
		if err != nil {
			return xerrors.Errorf("fetch updated template metadata: %w", err)
		}

		defaultTTL := time.Duration(req.DefaultTTLMillis) * time.Millisecond
		activityBump := time.Duration(req.ActivityBumpMillis) * time.Millisecond
		failureTTL := time.Duration(req.FailureTTLMillis) * time.Millisecond
		inactivityTTL := time.Duration(req.TimeTilDormantMillis) * time.Millisecond
		timeTilDormantAutoDelete := time.Duration(req.TimeTilDormantAutoDeleteMillis) * time.Millisecond

		if defaultTTL != time.Duration(template.DefaultTTL) ||
			activityBump != time.Duration(template.ActivityBump) ||
			autostopRequirementDaysOfWeekParsed != scheduleOpts.AutostopRequirement.DaysOfWeek ||
			autostartRequirementDaysOfWeekParsed != scheduleOpts.AutostartRequirement.DaysOfWeek ||
			req.AutostopRequirement.Weeks != scheduleOpts.AutostopRequirement.Weeks ||
			failureTTL != time.Duration(template.FailureTTL) ||
			inactivityTTL != time.Duration(template.TimeTilDormant) ||
			timeTilDormantAutoDelete != time.Duration(template.TimeTilDormantAutoDelete) ||
			req.AllowUserAutostart != template.AllowUserAutostart ||
			req.AllowUserAutostop != template.AllowUserAutostop {
			updated, err = (*api.TemplateScheduleStore.Load()).Set(ctx, tx, updated, schedule.TemplateScheduleOptions{
				// Some of these values are enterprise-only, but the
				// TemplateScheduleStore will handle avoiding setting them if
				// unlicensed.
				UserAutostartEnabled: req.AllowUserAutostart,
				UserAutostopEnabled:  req.AllowUserAutostop,
				DefaultTTL:           defaultTTL,
				ActivityBump:         activityBump,
				AutostopRequirement: schedule.TemplateAutostopRequirement{
					DaysOfWeek: autostopRequirementDaysOfWeekParsed,
					Weeks:      req.AutostopRequirement.Weeks,
				},
				AutostartRequirement: schedule.TemplateAutostartRequirement{
					DaysOfWeek: autostartRequirementDaysOfWeekParsed,
				},
				FailureTTL:                failureTTL,
				TimeTilDormant:            inactivityTTL,
				TimeTilDormantAutoDelete:  timeTilDormantAutoDelete,
				UpdateWorkspaceLastUsedAt: req.UpdateWorkspaceLastUsedAt,
				UpdateWorkspaceDormantAt:  req.UpdateWorkspaceDormantAt,
			})
			if err != nil {
				return xerrors.Errorf("set template schedule options: %w", err)
			}
		}

		return nil
	}, nil)
	if err != nil {
		httpapi.InternalServerError(rw, err)
		return
	}

	if updated.UpdatedAt.IsZero() {
		aReq.New = template
		httpapi.Write(ctx, rw, http.StatusNotModified, nil)
		return
	}
	aReq.New = updated

	httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(updated))
}

// @Summary Get template DAUs by ID
// @ID get-template-daus-by-id
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param template path string true "Template ID" format(uuid)
// @Success 200 {object} codersdk.DAUsResponse
// @Router /templates/{template}/daus [get]
func (api *API) templateDAUs(rw http.ResponseWriter, r *http.Request) {
	template := httpmw.TemplateParam(r)

	api.returnDAUsInternal(rw, r, []uuid.UUID{template.ID})
}

// @Summary Get template examples by organization
// @ID get-template-examples-by-organization
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param organization path string true "Organization ID" format(uuid)
// @Success 200 {array} codersdk.TemplateExample
// @Router /organizations/{organization}/templates/examples [get]
func (api *API) templateExamples(rw http.ResponseWriter, r *http.Request) {
	var (
		ctx          = r.Context()
		organization = httpmw.OrganizationParam(r)
	)

	if !api.Authorize(r, rbac.ActionRead, rbac.ResourceTemplate.InOrg(organization.ID)) {
		httpapi.ResourceNotFound(rw)
		return
	}

	ex, err := examples.List()
	if err != nil {
		httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error fetching examples.",
			Detail:  err.Error(),
		})
		return
	}

	httpapi.Write(ctx, rw, http.StatusOK, ex)
}

func (api *API) convertTemplates(templates []database.Template) []codersdk.Template {
	apiTemplates := make([]codersdk.Template, 0, len(templates))

	for _, template := range templates {
		apiTemplates = append(apiTemplates, api.convertTemplate(template))
	}

	// Sort templates by ActiveUserCount DESC
	sort.SliceStable(apiTemplates, func(i, j int) bool {
		return apiTemplates[i].ActiveUserCount > apiTemplates[j].ActiveUserCount
	})

	return apiTemplates
}

func (api *API) convertTemplate(
	template database.Template,
) codersdk.Template {
	templateAccessControl := (*(api.Options.AccessControlStore.Load())).GetTemplateAccessControl(template)

	owners := 0
	o, ok := api.metricsCache.TemplateWorkspaceOwners(template.ID)
	if ok {
		owners = o
	}

	buildTimeStats := api.metricsCache.TemplateBuildTimeStats(template.ID)

	autostopRequirementWeeks := template.AutostopRequirementWeeks
	if autostopRequirementWeeks < 1 {
		autostopRequirementWeeks = 1
	}

	return codersdk.Template{
		ID:                             template.ID,
		CreatedAt:                      template.CreatedAt,
		UpdatedAt:                      template.UpdatedAt,
		OrganizationID:                 template.OrganizationID,
		Name:                           template.Name,
		DisplayName:                    template.DisplayName,
		Provisioner:                    codersdk.ProvisionerType(template.Provisioner),
		ActiveVersionID:                template.ActiveVersionID,
		ActiveUserCount:                owners,
		BuildTimeStats:                 buildTimeStats,
		Description:                    template.Description,
		Icon:                           template.Icon,
		DefaultTTLMillis:               time.Duration(template.DefaultTTL).Milliseconds(),
		ActivityBumpMillis:             time.Duration(template.ActivityBump).Milliseconds(),
		CreatedByID:                    template.CreatedBy,
		CreatedByName:                  template.CreatedByUsername,
		AllowUserAutostart:             template.AllowUserAutostart,
		AllowUserAutostop:              template.AllowUserAutostop,
		AllowUserCancelWorkspaceJobs:   template.AllowUserCancelWorkspaceJobs,
		FailureTTLMillis:               time.Duration(template.FailureTTL).Milliseconds(),
		TimeTilDormantMillis:           time.Duration(template.TimeTilDormant).Milliseconds(),
		TimeTilDormantAutoDeleteMillis: time.Duration(template.TimeTilDormantAutoDelete).Milliseconds(),
		AutostopRequirement: codersdk.TemplateAutostopRequirement{
			DaysOfWeek: codersdk.BitmapToWeekdays(uint8(template.AutostopRequirementDaysOfWeek)),
			Weeks:      autostopRequirementWeeks,
		},
		AutostartRequirement: codersdk.TemplateAutostartRequirement{
			DaysOfWeek: codersdk.BitmapToWeekdays(template.AutostartAllowedDays()),
		},
		// These values depend on entitlements and come from the templateAccessControl
		RequireActiveVersion: templateAccessControl.RequireActiveVersion,
		Deprecated:           templateAccessControl.IsDeprecated(),
		DeprecationMessage:   templateAccessControl.Deprecated,
		MaxPortShareLevel:    codersdk.WorkspaceAgentPortShareLevel(template.MaxPortSharingLevel),
	}
}
