// SPDX-License-Identifier: GPL-3.0-or-later

package postgres

import (
	"fmt"
	"strings"
	"time"

	"github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi"
)

const (
	prioConnectionsUtilization = collectorapi.Priority + iota
	prioConnectionsUsage
	prioConnectionsStateCount
	prioDBConnectionsUtilization
	prioDBConnectionsCount

	prioTransactionsDuration
	prioDBTransactionsRatio
	prioDBTransactionsRate

	prioQueriesDuration

	prioDBOpsFetchedRowsRatio
	prioDBOpsReadRowsRate
	prioDBOpsWriteRowsRate
	prioDBTempFilesCreatedRate
	prioDBTempFilesIORate
	prioTableOpsRowsRate
	prioTableOpsRowsHOTRatio
	prioTableOpsRowsHOTRate
	prioTableScansRate
	prioTableScansRowsRate

	prioDBCacheIORatio
	prioDBIORate
	prioTableCacheIORatio
	prioTableIORate
	prioTableIndexCacheIORatio
	prioTableIndexIORate
	prioTableToastCacheIORatio
	prioTableToastIORate
	prioTableToastIndexCacheIORatio
	prioTableToastIndexIORate

	prioDBSize
	prioTableTotalSize
	prioIndexSize

	prioTableBloatSizePerc
	prioTableBloatSize
	prioIndexBloatSizePerc
	prioIndexBloatSize

	prioLocksUtilization
	prioDBLocksHeldCount
	prioDBLocksAwaitedCount
	prioDBDeadlocksRate

	prioAutovacuumWorkersCount
	prioTableAutovacuumSinceTime
	prioTableVacuumSinceTime
	prioTableAutoAnalyzeSinceTime
	prioTableLastAnalyzeAgo

	prioCheckpointsRate
	prioCheckpointsTime
	prioBGWriterHaltsRate
	prioBuffersIORate
	prioBuffersBackendFsyncRate
	prioBuffersAllocRate
	prioTXIDExhaustionTowardsAutovacuumPerc
	prioTXIDExhaustionPerc
	prioTXIDExhaustionOldestTXIDNum
	prioTableRowsDeadRatio
	prioTableRowsCount
	prioTableNullColumns
	prioIndexUsageStatus

	prioReplicationAppWALLagSize
	prioReplicationAppWALLagTime
	prioReplicationSlotFilesCount
	prioDBConflictsRate
	prioDBConflictsReasonRate

	prioWALIORate
	prioWALFilesCount
	prioWALArchivingFilesCount

	prioDatabasesCount
	prioCatalogRelationsCount
	prioCatalogRelationsSize

	prioUptime
)

var baseCharts = collectorapi.Charts{
	serverConnectionsUtilizationChart.Copy(),
	serverConnectionsUsageChart.Copy(),
	serverConnectionsStateCount.Copy(),
	locksUtilization.Copy(),
	checkpointsChart.Copy(),
	checkpointWriteChart.Copy(),
	buffersIORateChart.Copy(),
	buffersAllocRateChart.Copy(),
	bgWriterHaltsRateChart.Copy(),
	buffersBackendFsyncRateChart.Copy(),
	walIORateChart.Copy(),
	autovacuumWorkersCountChart.Copy(),
	txidExhaustionTowardsAutovacuumPercChart.Copy(),
	txidExhaustionPercChart.Copy(),
	txidExhaustionOldestTXIDNumChart.Copy(),

	catalogRelationSCountChart.Copy(),
	catalogRelationsSizeChart.Copy(),
	serverUptimeChart.Copy(),
	databasesCountChart.Copy(),
}

var walFilesCharts = collectorapi.Charts{
	walFilesCountChart.Copy(),
	walArchivingFilesCountChart.Copy(),
}

func (c *Collector) addWALFilesCharts() {
	charts := walFilesCharts.Copy()

	if err := c.Charts().Add(*charts...); err != nil {
		c.Warning(err)
	}
}

var (
	serverConnectionsUtilizationChart = collectorapi.Chart{
		ID:       "connections_utilization",
		Title:    "Connections utilization",
		Units:    "percentage",
		Fam:      "connections",
		Ctx:      "postgres.connections_utilization",
		Priority: prioConnectionsUtilization,
		Dims: collectorapi.Dims{
			{ID: "server_connections_utilization", Name: "used"},
		},
	}
	serverConnectionsUsageChart = collectorapi.Chart{
		ID:       "connections_usage",
		Title:    "Connections usage",
		Units:    "connections",
		Fam:      "connections",
		Ctx:      "postgres.connections_usage",
		Priority: prioConnectionsUsage,
		Type:     collectorapi.Stacked,
		Dims: collectorapi.Dims{
			{ID: "server_connections_available", Name: "available"},
			{ID: "server_connections_used", Name: "used"},
		},
	}
	serverConnectionsStateCount = collectorapi.Chart{
		ID:       "connections_state",
		Title:    "Connections in each state",
		Units:    "connections",
		Fam:      "connections",
		Ctx:      "postgres.connections_state_count",
		Priority: prioConnectionsStateCount,
		Type:     collectorapi.Stacked,
		Dims: collectorapi.Dims{
			{ID: "server_connections_state_active", Name: "active"},
			{ID: "server_connections_state_idle", Name: "idle"},
			{ID: "server_connections_state_idle_in_transaction", Name: "idle_in_transaction"},
			{ID: "server_connections_state_idle_in_transaction_aborted", Name: "idle_in_transaction_aborted"},
			{ID: "server_connections_state_fastpath_function_call", Name: "fastpath_function_call"},
			{ID: "server_connections_state_disabled", Name: "disabled"},
		},
	}

	locksUtilization = collectorapi.Chart{
		ID:       "locks_utilization",
		Title:    "Acquired locks utilization",
		Units:    "percentage",
		Fam:      "locks",
		Ctx:      "postgres.locks_utilization",
		Priority: prioLocksUtilization,
		Dims: collectorapi.Dims{
			{ID: "locks_utilization", Name: "used"},
		},
	}

	checkpointsChart = collectorapi.Chart{
		ID:       "checkpoints_rate",
		Title:    "Checkpoints",
		Units:    "checkpoints/s",
		Fam:      "maintenance",
		Ctx:      "postgres.checkpoints_rate",
		Priority: prioCheckpointsRate,
		Type:     collectorapi.Stacked,
		Dims: collectorapi.Dims{
			{ID: "checkpoints_timed", Name: "scheduled", Algo: collectorapi.Incremental},
			{ID: "checkpoints_req", Name: "requested", Algo: collectorapi.Incremental},
		},
	}
	// TODO: should be seconds, also it is units/s when using incremental...
	checkpointWriteChart = collectorapi.Chart{
		ID:       "checkpoints_time",
		Title:    "Checkpoint time",
		Units:    "milliseconds",
		Fam:      "maintenance",
		Ctx:      "postgres.checkpoints_time",
		Priority: prioCheckpointsTime,
		Type:     collectorapi.Stacked,
		Dims: collectorapi.Dims{
			{ID: "checkpoint_write_time", Name: "write", Algo: collectorapi.Incremental},
			{ID: "checkpoint_sync_time", Name: "sync", Algo: collectorapi.Incremental},
		},
	}
	bgWriterHaltsRateChart = collectorapi.Chart{
		ID:       "bgwriter_halts_rate",
		Title:    "Background writer scan halts",
		Units:    "halts/s",
		Fam:      "maintenance",
		Ctx:      "postgres.bgwriter_halts_rate",
		Priority: prioBGWriterHaltsRate,
		Dims: collectorapi.Dims{
			{ID: "maxwritten_clean", Name: "maxwritten", Algo: collectorapi.Incremental},
		},
	}

	buffersIORateChart = collectorapi.Chart{
		ID:       "buffers_io_rate",
		Title:    "Buffers written rate",
		Units:    "B/s",
		Fam:      "maintenance",
		Ctx:      "postgres.buffers_io_rate",
		Priority: prioBuffersIORate,
		Type:     collectorapi.Area,
		Dims: collectorapi.Dims{
			{ID: "buffers_checkpoint", Name: "checkpoint", Algo: collectorapi.Incremental},
			{ID: "buffers_backend", Name: "backend", Algo: collectorapi.Incremental},
			{ID: "buffers_clean", Name: "bgwriter", Algo: collectorapi.Incremental},
		},
	}
	buffersBackendFsyncRateChart = collectorapi.Chart{
		ID:       "buffers_backend_fsync_rate",
		Title:    "Backend fsync calls",
		Units:    "calls/s",
		Fam:      "maintenance",
		Ctx:      "postgres.buffers_backend_fsync_rate",
		Priority: prioBuffersBackendFsyncRate,
		Dims: collectorapi.Dims{
			{ID: "buffers_backend_fsync", Name: "fsync", Algo: collectorapi.Incremental},
		},
	}
	buffersAllocRateChart = collectorapi.Chart{
		ID:       "buffers_alloc_rate",
		Title:    "Buffers allocated",
		Units:    "B/s",
		Fam:      "maintenance",
		Ctx:      "postgres.buffers_allocated_rate",
		Priority: prioBuffersAllocRate,
		Dims: collectorapi.Dims{
			{ID: "buffers_alloc", Name: "allocated", Algo: collectorapi.Incremental},
		},
	}

	walIORateChart = collectorapi.Chart{
		ID:       "wal_io_rate",
		Title:    "Write-Ahead Log writes",
		Units:    "B/s",
		Fam:      "wal",
		Ctx:      "postgres.wal_io_rate",
		Priority: prioWALIORate,
		Dims: collectorapi.Dims{
			{ID: "wal_writes", Name: "written", Algo: collectorapi.Incremental},
		},
	}
	walFilesCountChart = collectorapi.Chart{
		ID:       "wal_files_count",
		Title:    "Write-Ahead Log files",
		Units:    "files",
		Fam:      "wal",
		Ctx:      "postgres.wal_files_count",
		Priority: prioWALFilesCount,
		Type:     collectorapi.Stacked,
		Dims: collectorapi.Dims{
			{ID: "wal_written_files", Name: "written"},
			{ID: "wal_recycled_files", Name: "recycled"},
		},
	}

	walArchivingFilesCountChart = collectorapi.Chart{
		ID:       "wal_archiving_files_count",
		Title:    "Write-Ahead Log archived files",
		Units:    "files/s",
		Fam:      "wal",
		Ctx:      "postgres.wal_archiving_files_count",
		Priority: prioWALArchivingFilesCount,
		Type:     collectorapi.Stacked,
		Dims: collectorapi.Dims{
			{ID: "wal_archive_files_ready_count", Name: "ready"},
			{ID: "wal_archive_files_done_count", Name: "done"},
		},
	}

	autovacuumWorkersCountChart = collectorapi.Chart{
		ID:       "autovacuum_workers_count",
		Title:    "Autovacuum workers",
		Units:    "workers",
		Fam:      "vacuum and analyze",
		Ctx:      "postgres.autovacuum_workers_count",
		Priority: prioAutovacuumWorkersCount,
		Dims: collectorapi.Dims{
			{ID: "autovacuum_analyze", Name: "analyze"},
			{ID: "autovacuum_vacuum_analyze", Name: "vacuum_analyze"},
			{ID: "autovacuum_vacuum", Name: "vacuum"},
			{ID: "autovacuum_vacuum_freeze", Name: "vacuum_freeze"},
			{ID: "autovacuum_brin_summarize", Name: "brin_summarize"},
		},
	}

	txidExhaustionTowardsAutovacuumPercChart = collectorapi.Chart{
		ID:       "txid_exhaustion_towards_autovacuum_perc",
		Title:    "Percent towards emergency autovacuum",
		Units:    "percentage",
		Fam:      "maintenance",
		Ctx:      "postgres.txid_exhaustion_towards_autovacuum_perc",
		Priority: prioTXIDExhaustionTowardsAutovacuumPerc,
		Dims: collectorapi.Dims{
			{ID: "percent_towards_emergency_autovacuum", Name: "emergency_autovacuum"},
		},
	}
	txidExhaustionPercChart = collectorapi.Chart{
		ID:       "txid_exhaustion_perc",
		Title:    "Percent towards transaction ID wraparound",
		Units:    "percentage",
		Fam:      "maintenance",
		Ctx:      "postgres.txid_exhaustion_perc",
		Priority: prioTXIDExhaustionPerc,
		Dims: collectorapi.Dims{
			{ID: "percent_towards_wraparound", Name: "txid_exhaustion"},
		},
	}
	txidExhaustionOldestTXIDNumChart = collectorapi.Chart{
		ID:       "txid_exhaustion_oldest_txid_num",
		Title:    "Oldest transaction XID",
		Units:    "xid",
		Fam:      "maintenance",
		Ctx:      "postgres.txid_exhaustion_oldest_txid_num",
		Priority: prioTXIDExhaustionOldestTXIDNum,
		Dims: collectorapi.Dims{
			{ID: "oldest_current_xid", Name: "xid"},
		},
	}

	catalogRelationSCountChart = collectorapi.Chart{
		ID:       "catalog_relations_count",
		Title:    "Relation count",
		Units:    "relations",
		Fam:      "catalog",
		Ctx:      "postgres.catalog_relations_count",
		Priority: prioCatalogRelationsCount,
		Type:     collectorapi.Stacked,
		Dims: collectorapi.Dims{
			{ID: "catalog_relkind_r_count", Name: "ordinary_table"},
			{ID: "catalog_relkind_i_count", Name: "index"},
			{ID: "catalog_relkind_S_count", Name: "sequence"},
			{ID: "catalog_relkind_t_count", Name: "toast_table"},
			{ID: "catalog_relkind_v_count", Name: "view"},
			{ID: "catalog_relkind_m_count", Name: "materialized_view"},
			{ID: "catalog_relkind_c_count", Name: "composite_type"},
			{ID: "catalog_relkind_f_count", Name: "foreign_table"},
			{ID: "catalog_relkind_p_count", Name: "partitioned_table"},
			{ID: "catalog_relkind_I_count", Name: "partitioned_index"},
		},
	}
	catalogRelationsSizeChart = collectorapi.Chart{
		ID:       "catalog_relations_size",
		Title:    "Relation size",
		Units:    "B",
		Fam:      "catalog",
		Ctx:      "postgres.catalog_relations_size",
		Priority: prioCatalogRelationsSize,
		Type:     collectorapi.Stacked,
		Dims: collectorapi.Dims{
			{ID: "catalog_relkind_r_size", Name: "ordinary_table"},
			{ID: "catalog_relkind_i_size", Name: "index"},
			{ID: "catalog_relkind_S_size", Name: "sequence"},
			{ID: "catalog_relkind_t_size", Name: "toast_table"},
			{ID: "catalog_relkind_v_size", Name: "view"},
			{ID: "catalog_relkind_m_size", Name: "materialized_view"},
			{ID: "catalog_relkind_c_size", Name: "composite_type"},
			{ID: "catalog_relkind_f_size", Name: "foreign_table"},
			{ID: "catalog_relkind_p_size", Name: "partitioned_table"},
			{ID: "catalog_relkind_I_size", Name: "partitioned_index"},
		},
	}

	serverUptimeChart = collectorapi.Chart{
		ID:       "server_uptime",
		Title:    "Uptime",
		Units:    "seconds",
		Fam:      "uptime",
		Ctx:      "postgres.uptime",
		Priority: prioUptime,
		Dims: collectorapi.Dims{
			{ID: "server_uptime", Name: "uptime"},
		},
	}

	databasesCountChart = collectorapi.Chart{
		ID:       "databases_count",
		Title:    "Number of databases",
		Units:    "databases",
		Fam:      "catalog",
		Ctx:      "postgres.databases_count",
		Priority: prioDatabasesCount,
		Dims: collectorapi.Dims{
			{ID: "databases_count", Name: "databases"},
		},
	}

	transactionsDurationChartTmpl = collectorapi.Chart{
		ID:       "transactions_duration",
		Title:    "Observed transactions time",
		Units:    "transactions/s",
		Fam:      "transactions",
		Ctx:      "postgres.transactions_duration",
		Priority: prioTransactionsDuration,
		Type:     collectorapi.Stacked,
	}
	queriesDurationChartTmpl = collectorapi.Chart{
		ID:       "queries_duration",
		Title:    "Observed active queries time",
		Units:    "queries/s",
		Fam:      "queries",
		Ctx:      "postgres.queries_duration",
		Priority: prioQueriesDuration,
		Type:     collectorapi.Stacked,
	}
)

func newRunningTimeHistogramChart(tmpl collectorapi.Chart, prefix string, buckets []float64) (*collectorapi.Chart, error) {
	chart := tmpl.Copy()

	for i, v := range buckets {
		dim := &collectorapi.Dim{
			ID:   fmt.Sprintf("%s_hist_bucket_%d", prefix, i+1),
			Name: time.Duration(v * float64(time.Second)).String(),
			Algo: collectorapi.Incremental,
		}
		if err := chart.AddDim(dim); err != nil {
			return nil, err
		}
	}

	dim := &collectorapi.Dim{
		ID:   fmt.Sprintf("%s_hist_bucket_inf", prefix),
		Name: "+Inf",
		Algo: collectorapi.Incremental,
	}
	if err := chart.AddDim(dim); err != nil {
		return nil, err
	}

	return chart, nil
}

func (c *Collector) addTransactionsRunTimeHistogramChart() {
	chart, err := newRunningTimeHistogramChart(
		transactionsDurationChartTmpl,
		"transaction_running_time",
		c.XactTimeHistogram,
	)
	if err != nil {
		c.Warning(err)
		return
	}
	if err := c.Charts().Add(chart); err != nil {
		c.Warning(err)
	}
}

func (c *Collector) addQueriesRunTimeHistogramChart() {
	chart, err := newRunningTimeHistogramChart(
		queriesDurationChartTmpl,
		"query_running_time",
		c.QueryTimeHistogram,
	)
	if err != nil {
		c.Warning(err)
		return
	}
	if err := c.Charts().Add(chart); err != nil {
		c.Warning(err)
	}
}

var (
	replicationStandbyAppCharts = collectorapi.Charts{
		replicationAppWALLagSizeChartTmpl.Copy(),
		replicationAppWALLagTimeChartTmpl.Copy(),
	}
	replicationAppWALLagSizeChartTmpl = collectorapi.Chart{
		ID:       "replication_app_%s_wal_lag_size",
		Title:    "Standby application WAL lag size",
		Units:    "B",
		Fam:      "replication",
		Ctx:      "postgres.replication_app_wal_lag_size",
		Priority: prioReplicationAppWALLagSize,
		Dims: collectorapi.Dims{
			{ID: "repl_standby_app_%s_wal_sent_lag_size", Name: "sent_lag"},
			{ID: "repl_standby_app_%s_wal_write_lag_size", Name: "write_lag"},
			{ID: "repl_standby_app_%s_wal_flush_lag_size", Name: "flush_lag"},
			{ID: "repl_standby_app_%s_wal_replay_lag_size", Name: "replay_lag"},
		},
	}
	replicationAppWALLagTimeChartTmpl = collectorapi.Chart{
		ID:       "replication_app_%s_wal_lag_time",
		Title:    "Standby application WAL lag time",
		Units:    "seconds",
		Fam:      "replication",
		Ctx:      "postgres.replication_app_wal_lag_time",
		Priority: prioReplicationAppWALLagTime,
		Dims: collectorapi.Dims{
			{ID: "repl_standby_app_%s_wal_write_lag_time", Name: "write_lag"},
			{ID: "repl_standby_app_%s_wal_flush_lag_time", Name: "flush_lag"},
			{ID: "repl_standby_app_%s_wal_replay_lag_time", Name: "replay_lag"},
		},
	}
)

func newReplicationStandbyAppCharts(app string) *collectorapi.Charts {
	charts := replicationStandbyAppCharts.Copy()
	for _, c := range *charts {
		c.ID = fmt.Sprintf(c.ID, app)
		c.Labels = []collectorapi.Label{
			{Key: "application", Value: app},
		}
		for _, d := range c.Dims {
			d.ID = fmt.Sprintf(d.ID, app)
		}
	}
	return charts
}

func (c *Collector) addNewReplicationStandbyAppCharts(app string) {
	charts := newReplicationStandbyAppCharts(app)
	if err := c.Charts().Add(*charts...); err != nil {
		c.Warning(err)
	}
}

func (c *Collector) removeReplicationStandbyAppCharts(app string) {
	prefix := fmt.Sprintf("replication_standby_app_%s_", app)
	for _, c := range *c.Charts() {
		if strings.HasPrefix(c.ID, prefix) {
			c.MarkRemove()
			c.MarkNotCreated()
		}
	}
}

var (
	replicationSlotCharts = collectorapi.Charts{
		replicationSlotFilesCountChartTmpl.Copy(),
	}
	replicationSlotFilesCountChartTmpl = collectorapi.Chart{
		ID:       "replication_slot_%s_files_count",
		Title:    "Replication slot files",
		Units:    "files",
		Fam:      "replication",
		Ctx:      "postgres.replication_slot_files_count",
		Priority: prioReplicationSlotFilesCount,
		Dims: collectorapi.Dims{
			{ID: "repl_slot_%s_replslot_wal_keep", Name: "wal_keep"},
			{ID: "repl_slot_%s_replslot_files", Name: "pg_replslot_files"},
		},
	}
)

func newReplicationSlotCharts(slot string) *collectorapi.Charts {
	charts := replicationSlotCharts.Copy()
	for _, c := range *charts {
		c.ID = fmt.Sprintf(c.ID, slot)
		c.Labels = []collectorapi.Label{
			{Key: "slot", Value: slot},
		}
		for _, d := range c.Dims {
			d.ID = fmt.Sprintf(d.ID, slot)
		}
	}
	return charts
}

func (c *Collector) addNewReplicationSlotCharts(slot string) {
	charts := newReplicationSlotCharts(slot)
	if err := c.Charts().Add(*charts...); err != nil {
		c.Warning(err)
	}
}

func (c *Collector) removeReplicationSlotCharts(slot string) {
	prefix := fmt.Sprintf("replication_slot_%s_", slot)
	for _, c := range *c.Charts() {
		if strings.HasPrefix(c.ID, prefix) {
			c.MarkRemove()
			c.MarkNotCreated()
		}
	}
}

var (
	dbChartsTmpl = collectorapi.Charts{
		dbTransactionsRatioChartTmpl.Copy(),
		dbTransactionsRateChartTmpl.Copy(),
		dbConnectionsUtilizationChartTmpl.Copy(),
		dbConnectionsCountChartTmpl.Copy(),
		dbCacheIORatioChartTmpl.Copy(),
		dbIORateChartTmpl.Copy(),
		dbOpsFetchedRowsRatioChartTmpl.Copy(),
		dbOpsReadRowsRateChartTmpl.Copy(),
		dbOpsWriteRowsRateChartTmpl.Copy(),
		dbDeadlocksRateChartTmpl.Copy(),
		dbLocksHeldCountChartTmpl.Copy(),
		dbLocksAwaitedCountChartTmpl.Copy(),
		dbTempFilesCreatedRateChartTmpl.Copy(),
		dbTempFilesIORateChartTmpl.Copy(),
		dbSizeChartTmpl.Copy(),
	}
	dbTransactionsRatioChartTmpl = collectorapi.Chart{
		ID:       "db_%s_transactions_ratio",
		Title:    "Database transactions ratio",
		Units:    "percentage",
		Fam:      "transactions",
		Ctx:      "postgres.db_transactions_ratio",
		Priority: prioDBTransactionsRatio,
		Type:     collectorapi.Stacked,
		Dims: collectorapi.Dims{
			{ID: "db_%s_xact_commit", Name: "committed", Algo: collectorapi.PercentOfIncremental},
			{ID: "db_%s_xact_rollback", Name: "rollback", Algo: collectorapi.PercentOfIncremental},
		},
	}
	dbTransactionsRateChartTmpl = collectorapi.Chart{
		ID:       "db_%s_transactions_rate",
		Title:    "Database transactions",
		Units:    "transactions/s",
		Fam:      "transactions",
		Ctx:      "postgres.db_transactions_rate",
		Priority: prioDBTransactionsRate,
		Dims: collectorapi.Dims{
			{ID: "db_%s_xact_commit", Name: "committed", Algo: collectorapi.Incremental},
			{ID: "db_%s_xact_rollback", Name: "rollback", Algo: collectorapi.Incremental},
		},
	}
	dbConnectionsUtilizationChartTmpl = collectorapi.Chart{
		ID:       "db_%s_connections_utilization",
		Title:    "Database connections utilization",
		Units:    "percentage",
		Fam:      "connections",
		Ctx:      "postgres.db_connections_utilization",
		Priority: prioDBConnectionsUtilization,
		Dims: collectorapi.Dims{
			{ID: "db_%s_numbackends_utilization", Name: "used"},
		},
	}
	dbConnectionsCountChartTmpl = collectorapi.Chart{
		ID:       "db_%s_connections",
		Title:    "Database connections",
		Units:    "connections",
		Fam:      "connections",
		Ctx:      "postgres.db_connections_count",
		Priority: prioDBConnectionsCount,
		Dims: collectorapi.Dims{
			{ID: "db_%s_numbackends", Name: "connections"},
		},
	}
	dbCacheIORatioChartTmpl = collectorapi.Chart{
		ID:       "db_%s_cache_io_ratio",
		Title:    "Database buffer cache miss ratio",
		Units:    "percentage",
		Fam:      "cache",
		Ctx:      "postgres.db_cache_io_ratio",
		Priority: prioDBCacheIORatio,
		Dims: collectorapi.Dims{
			{ID: "db_%s_blks_read_perc", Name: "miss"},
		},
	}
	dbIORateChartTmpl = collectorapi.Chart{
		ID:       "db_%s_io_rate",
		Title:    "Database reads",
		Units:    "B/s",
		Fam:      "cache",
		Ctx:      "postgres.db_io_rate",
		Priority: prioDBIORate,
		Type:     collectorapi.Area,
		Dims: collectorapi.Dims{
			{ID: "db_%s_blks_hit", Name: "memory", Algo: collectorapi.Incremental},
			{ID: "db_%s_blks_read", Name: "disk", Algo: collectorapi.Incremental},
		},
	}
	dbOpsFetchedRowsRatioChartTmpl = collectorapi.Chart{
		ID:       "db_%s_db_ops_fetched_rows_ratio",
		Title:    "Database rows fetched ratio",
		Units:    "percentage",
		Fam:      "throughput",
		Ctx:      "postgres.db_ops_fetched_rows_ratio",
		Priority: prioDBOpsFetchedRowsRatio,
		Dims: collectorapi.Dims{
			{ID: "db_%s_tup_fetched_perc", Name: "fetched"},
		},
	}
	dbOpsReadRowsRateChartTmpl = collectorapi.Chart{
		ID:       "db_%s_ops_read_rows_rate",
		Title:    "Database rows read",
		Units:    "rows/s",
		Fam:      "throughput",
		Ctx:      "postgres.db_ops_read_rows_rate",
		Priority: prioDBOpsReadRowsRate,
		Dims: collectorapi.Dims{
			{ID: "db_%s_tup_returned", Name: "returned", Algo: collectorapi.Incremental},
			{ID: "db_%s_tup_fetched", Name: "fetched", Algo: collectorapi.Incremental},
		},
	}
	dbOpsWriteRowsRateChartTmpl = collectorapi.Chart{
		ID:       "db_%s_ops_write_rows_rate",
		Title:    "Database rows written",
		Units:    "rows/s",
		Fam:      "throughput",
		Ctx:      "postgres.db_ops_write_rows_rate",
		Priority: prioDBOpsWriteRowsRate,
		Dims: collectorapi.Dims{
			{ID: "db_%s_tup_inserted", Name: "inserted", Algo: collectorapi.Incremental},
			{ID: "db_%s_tup_deleted", Name: "deleted", Algo: collectorapi.Incremental},
			{ID: "db_%s_tup_updated", Name: "updated", Algo: collectorapi.Incremental},
		},
	}
	dbConflictsRateChartTmpl = collectorapi.Chart{
		ID:       "db_%s_conflicts_rate",
		Title:    "Database canceled queries",
		Units:    "queries/s",
		Fam:      "replication",
		Ctx:      "postgres.db_conflicts_rate",
		Priority: prioDBConflictsRate,
		Dims: collectorapi.Dims{
			{ID: "db_%s_conflicts", Name: "conflicts", Algo: collectorapi.Incremental},
		},
	}
	dbConflictsReasonRateChartTmpl = collectorapi.Chart{
		ID:       "db_%s_conflicts_reason_rate",
		Title:    "Database canceled queries by reason",
		Units:    "queries/s",
		Fam:      "replication",
		Ctx:      "postgres.db_conflicts_reason_rate",
		Priority: prioDBConflictsReasonRate,
		Dims: collectorapi.Dims{
			{ID: "db_%s_confl_tablespace", Name: "tablespace", Algo: collectorapi.Incremental},
			{ID: "db_%s_confl_lock", Name: "lock", Algo: collectorapi.Incremental},
			{ID: "db_%s_confl_snapshot", Name: "snapshot", Algo: collectorapi.Incremental},
			{ID: "db_%s_confl_bufferpin", Name: "bufferpin", Algo: collectorapi.Incremental},
			{ID: "db_%s_confl_deadlock", Name: "deadlock", Algo: collectorapi.Incremental},
		},
	}
	dbDeadlocksRateChartTmpl = collectorapi.Chart{
		ID:       "db_%s_deadlocks_rate",
		Title:    "Database deadlocks",
		Units:    "deadlocks/s",
		Fam:      "locks",
		Ctx:      "postgres.db_deadlocks_rate",
		Priority: prioDBDeadlocksRate,
		Dims: collectorapi.Dims{
			{ID: "db_%s_deadlocks", Name: "deadlocks", Algo: collectorapi.Incremental},
		},
	}
	dbLocksHeldCountChartTmpl = collectorapi.Chart{
		ID:       "db_%s_locks_held",
		Title:    "Database locks held",
		Units:    "locks",
		Fam:      "locks",
		Ctx:      "postgres.db_locks_held_count",
		Priority: prioDBLocksHeldCount,
		Type:     collectorapi.Stacked,
		Dims: collectorapi.Dims{
			{ID: "db_%s_lock_mode_AccessShareLock_held", Name: "access_share"},
			{ID: "db_%s_lock_mode_RowShareLock_held", Name: "row_share"},
			{ID: "db_%s_lock_mode_RowExclusiveLock_held", Name: "row_exclusive"},
			{ID: "db_%s_lock_mode_ShareUpdateExclusiveLock_held", Name: "share_update"},
			{ID: "db_%s_lock_mode_ShareLock_held", Name: "share"},
			{ID: "db_%s_lock_mode_ShareRowExclusiveLock_held", Name: "share_row_exclusive"},
			{ID: "db_%s_lock_mode_ExclusiveLock_held", Name: "exclusive"},
			{ID: "db_%s_lock_mode_AccessExclusiveLock_held", Name: "access_exclusive"},
		},
	}
	dbLocksAwaitedCountChartTmpl = collectorapi.Chart{
		ID:       "db_%s_locks_awaited_count",
		Title:    "Database locks awaited",
		Units:    "locks",
		Fam:      "locks",
		Ctx:      "postgres.db_locks_awaited_count",
		Priority: prioDBLocksAwaitedCount,
		Type:     collectorapi.Stacked,
		Dims: collectorapi.Dims{
			{ID: "db_%s_lock_mode_AccessShareLock_awaited", Name: "access_share"},
			{ID: "db_%s_lock_mode_RowShareLock_awaited", Name: "row_share"},
			{ID: "db_%s_lock_mode_RowExclusiveLock_awaited", Name: "row_exclusive"},
			{ID: "db_%s_lock_mode_ShareUpdateExclusiveLock_awaited", Name: "share_update"},
			{ID: "db_%s_lock_mode_ShareLock_awaited", Name: "share"},
			{ID: "db_%s_lock_mode_ShareRowExclusiveLock_awaited", Name: "share_row_exclusive"},
			{ID: "db_%s_lock_mode_ExclusiveLock_awaited", Name: "exclusive"},
			{ID: "db_%s_lock_mode_AccessExclusiveLock_awaited", Name: "access_exclusive"},
		},
	}
	dbTempFilesCreatedRateChartTmpl = collectorapi.Chart{
		ID:       "db_%s_temp_files_files_created_rate",
		Title:    "Database created temporary files",
		Units:    "files/s",
		Fam:      "throughput",
		Ctx:      "postgres.db_temp_files_created_rate",
		Priority: prioDBTempFilesCreatedRate,
		Dims: collectorapi.Dims{
			{ID: "db_%s_temp_files", Name: "created", Algo: collectorapi.Incremental},
		},
	}
	dbTempFilesIORateChartTmpl = collectorapi.Chart{
		ID:       "db_%s_temp_files_io_rate",
		Title:    "Database temporary files data written to disk",
		Units:    "B/s",
		Fam:      "throughput",
		Ctx:      "postgres.db_temp_files_io_rate",
		Priority: prioDBTempFilesIORate,
		Dims: collectorapi.Dims{
			{ID: "db_%s_temp_bytes", Name: "written", Algo: collectorapi.Incremental},
		},
	}
	dbSizeChartTmpl = collectorapi.Chart{
		ID:       "db_%s_size",
		Title:    "Database size",
		Units:    "B",
		Fam:      "size",
		Ctx:      "postgres.db_size",
		Priority: prioDBSize,
		Dims: collectorapi.Dims{
			{ID: "db_%s_size", Name: "size"},
		},
	}
)

func (c *Collector) addDBConflictsCharts(db *dbMetrics) {
	tmpl := collectorapi.Charts{
		dbConflictsRateChartTmpl.Copy(),
		dbConflictsReasonRateChartTmpl.Copy(),
	}
	charts := newDatabaseCharts(tmpl.Copy(), db)

	if err := c.Charts().Add(*charts...); err != nil {
		c.Warning(err)
	}
}

func newDatabaseCharts(tmpl *collectorapi.Charts, db *dbMetrics) *collectorapi.Charts {
	charts := tmpl.Copy()
	for _, c := range *charts {
		c.ID = fmt.Sprintf(c.ID, db.name)
		c.Labels = []collectorapi.Label{
			{Key: "database", Value: db.name},
		}
		for _, d := range c.Dims {
			d.ID = fmt.Sprintf(d.ID, db.name)
		}
	}
	return charts
}

func (c *Collector) addNewDatabaseCharts(db *dbMetrics) {
	charts := newDatabaseCharts(dbChartsTmpl.Copy(), db)

	if db.size == nil {
		_ = charts.Remove(fmt.Sprintf(dbSizeChartTmpl.ID, db.name))
	}

	if err := c.Charts().Add(*charts...); err != nil {
		c.Warning(err)
	}
}

func (c *Collector) removeDatabaseCharts(db *dbMetrics) {
	prefix := fmt.Sprintf("db_%s_", db.name)
	for _, c := range *c.Charts() {
		if strings.HasPrefix(c.ID, prefix) {
			c.MarkRemove()
			c.MarkNotCreated()
		}
	}
}

var (
	tableChartsTmpl = collectorapi.Charts{
		tableRowsCountChartTmpl.Copy(),
		tableDeadRowsDeadRatioChartTmpl.Copy(),
		tableOpsRowsRateChartTmpl.Copy(),
		tableOpsRowsHOTRatioChartTmpl.Copy(),
		tableOpsRowsHOTRateChartTmpl.Copy(),
		tableScansRateChartTmpl.Copy(),
		tableScansRowsRateChartTmpl.Copy(),
		tableNullColumnsCountChartTmpl.Copy(),
		tableTotalSizeChartTmpl.Copy(),
		tableBloatSizePercChartTmpl.Copy(),
		tableBloatSizeChartTmpl.Copy(),
	}

	tableDeadRowsDeadRatioChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_rows_dead_ratio",
		Title:    "Table dead rows",
		Units:    "%",
		Fam:      "maintenance",
		Ctx:      "postgres.table_rows_dead_ratio",
		Priority: prioTableRowsDeadRatio,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_n_dead_tup_perc", Name: "dead"},
		},
	}
	tableRowsCountChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_rows_count",
		Title:    "Table total rows",
		Units:    "rows",
		Fam:      "maintenance",
		Ctx:      "postgres.table_rows_count",
		Priority: prioTableRowsCount,
		Type:     collectorapi.Stacked,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_n_live_tup", Name: "live"},
			{ID: "table_%s_db_%s_schema_%s_n_dead_tup", Name: "dead"},
		},
	}
	tableOpsRowsRateChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_ops_rows_rate",
		Title:    "Table throughput",
		Units:    "rows/s",
		Fam:      "throughput",
		Ctx:      "postgres.table_ops_rows_rate",
		Priority: prioTableOpsRowsRate,
		Type:     collectorapi.Stacked,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_n_tup_ins", Name: "inserted", Algo: collectorapi.Incremental},
			{ID: "table_%s_db_%s_schema_%s_n_tup_del", Name: "deleted", Algo: collectorapi.Incremental},
			{ID: "table_%s_db_%s_schema_%s_n_tup_upd", Name: "updated", Algo: collectorapi.Incremental},
		},
	}
	tableOpsRowsHOTRatioChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_ops_rows_hot_ratio",
		Title:    "Table HOT updates ratio",
		Units:    "percentage",
		Fam:      "throughput",
		Ctx:      "postgres.table_ops_rows_hot_ratio",
		Priority: prioTableOpsRowsHOTRatio,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_n_tup_hot_upd_perc", Name: "hot"},
		},
	}
	tableOpsRowsHOTRateChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_ops_rows_hot_rate",
		Title:    "Table HOT updates",
		Units:    "rows/s",
		Fam:      "throughput",
		Ctx:      "postgres.table_ops_rows_hot_rate",
		Priority: prioTableOpsRowsHOTRate,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_n_tup_hot_upd", Name: "hot", Algo: collectorapi.Incremental},
		},
	}
	tableCacheIORatioChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_cache_io_ratio",
		Title:    "Table I/O cache miss ratio",
		Units:    "percentage",
		Fam:      "cache",
		Ctx:      "postgres.table_cache_io_ratio",
		Priority: prioTableCacheIORatio,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_heap_blks_read_perc", Name: "miss"},
		},
	}
	tableIORateChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_io_rate",
		Title:    "Table I/O",
		Units:    "B/s",
		Fam:      "cache",
		Ctx:      "postgres.table_io_rate",
		Priority: prioTableIORate,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_heap_blks_hit", Name: "memory", Algo: collectorapi.Incremental},
			{ID: "table_%s_db_%s_schema_%s_heap_blks_read", Name: "disk", Algo: collectorapi.Incremental},
		},
	}
	tableIndexCacheIORatioChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_index_cache_io_ratio",
		Title:    "Table index I/O cache miss ratio",
		Units:    "percentage",
		Fam:      "cache",
		Ctx:      "postgres.table_index_cache_io_ratio",
		Priority: prioTableIndexCacheIORatio,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_idx_blks_read_perc", Name: "miss", Algo: collectorapi.Incremental},
		},
	}
	tableIndexIORateChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_index_io_rate",
		Title:    "Table index I/O",
		Units:    "B/s",
		Fam:      "cache",
		Ctx:      "postgres.table_index_io_rate",
		Priority: prioTableIndexIORate,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_idx_blks_hit", Name: "memory", Algo: collectorapi.Incremental},
			{ID: "table_%s_db_%s_schema_%s_idx_blks_read", Name: "disk", Algo: collectorapi.Incremental},
		},
	}
	tableTOASCacheIORatioChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_toast_cache_io_ratio",
		Title:    "Table TOAST I/O cache miss ratio",
		Units:    "percentage",
		Fam:      "cache",
		Ctx:      "postgres.table_toast_cache_io_ratio",
		Priority: prioTableToastCacheIORatio,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_toast_blks_read_perc", Name: "miss", Algo: collectorapi.Incremental},
		},
	}
	tableTOASTIORateChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_toast_io_rate",
		Title:    "Table TOAST I/O",
		Units:    "B/s",
		Fam:      "cache",
		Ctx:      "postgres.table_toast_io_rate",
		Priority: prioTableToastIORate,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_toast_blks_hit", Name: "memory", Algo: collectorapi.Incremental},
			{ID: "table_%s_db_%s_schema_%s_toast_blks_read", Name: "disk", Algo: collectorapi.Incremental},
		},
	}
	tableTOASTIndexCacheIORatioChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_toast_index_cache_io_ratio",
		Title:    "Table TOAST index I/O cache miss ratio",
		Units:    "percentage",
		Fam:      "cache",
		Ctx:      "postgres.table_toast_index_cache_io_ratio",
		Priority: prioTableToastIndexCacheIORatio,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_tidx_blks_read_perc", Name: "miss", Algo: collectorapi.Incremental},
		},
	}
	tableTOASTIndexIORateChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_toast_index_io_rate",
		Title:    "Table TOAST index I/O",
		Units:    "B/s",
		Fam:      "cache",
		Ctx:      "postgres.table_toast_index_io_rate",
		Priority: prioTableToastIndexIORate,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_tidx_blks_hit", Name: "memory", Algo: collectorapi.Incremental},
			{ID: "table_%s_db_%s_schema_%s_tidx_blks_read", Name: "disk", Algo: collectorapi.Incremental},
		},
	}
	tableScansRateChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_scans_rate",
		Title:    "Table scans",
		Units:    "scans/s",
		Fam:      "throughput",
		Ctx:      "postgres.table_scans_rate",
		Priority: prioTableScansRate,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_idx_scan", Name: "index", Algo: collectorapi.Incremental},
			{ID: "table_%s_db_%s_schema_%s_seq_scan", Name: "sequential", Algo: collectorapi.Incremental},
		},
	}
	tableScansRowsRateChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_scans_rows_rate",
		Title:    "Table live rows fetched by scans",
		Units:    "rows/s",
		Fam:      "throughput",
		Ctx:      "postgres.table_scans_rows_rate",
		Priority: prioTableScansRowsRate,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_idx_tup_fetch", Name: "index", Algo: collectorapi.Incremental},
			{ID: "table_%s_db_%s_schema_%s_seq_tup_read", Name: "sequential", Algo: collectorapi.Incremental},
		},
	}
	tableAutoVacuumSinceTimeChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_autovacuum_since_time",
		Title:    "Table time since last auto VACUUM",
		Units:    "seconds",
		Fam:      "vacuum and analyze",
		Ctx:      "postgres.table_autovacuum_since_time",
		Priority: prioTableAutovacuumSinceTime,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_last_autovacuum_ago", Name: "time"},
		},
	}
	tableVacuumSinceTimeChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_vacuum_since_time",
		Title:    "Table time since last manual VACUUM",
		Units:    "seconds",
		Fam:      "vacuum and analyze",
		Ctx:      "postgres.table_vacuum_since_time",
		Priority: prioTableVacuumSinceTime,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_last_vacuum_ago", Name: "time"},
		},
	}
	tableAutoAnalyzeSinceTimeChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_autoanalyze_since_time",
		Title:    "Table time since last auto ANALYZE",
		Units:    "seconds",
		Fam:      "vacuum and analyze",
		Ctx:      "postgres.table_autoanalyze_since_time",
		Priority: prioTableAutoAnalyzeSinceTime,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_last_autoanalyze_ago", Name: "time"},
		},
	}
	tableAnalyzeSinceTimeChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_analyze_since_time",
		Title:    "Table time since last manual ANALYZE",
		Units:    "seconds",
		Fam:      "vacuum and analyze",
		Ctx:      "postgres.table_analyze_since_time",
		Priority: prioTableLastAnalyzeAgo,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_last_analyze_ago", Name: "time"},
		},
	}
	tableNullColumnsCountChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_null_columns_count",
		Title:    "Table null columns",
		Units:    "columns",
		Fam:      "maintenance",
		Ctx:      "postgres.table_null_columns_count",
		Priority: prioTableNullColumns,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_null_columns", Name: "null"},
		},
	}
	tableTotalSizeChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_total_size",
		Title:    "Table total size",
		Units:    "B",
		Fam:      "size",
		Ctx:      "postgres.table_total_size",
		Priority: prioTableTotalSize,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_total_size", Name: "size"},
		},
	}
	tableBloatSizePercChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_bloat_size_perc",
		Title:    "Table bloat size percentage",
		Units:    "percentage",
		Fam:      "bloat",
		Ctx:      "postgres.table_bloat_size_perc",
		Priority: prioTableBloatSizePerc,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_bloat_size_perc", Name: "bloat"},
		},
		Vars: collectorapi.Vars{
			{ID: "table_%s_db_%s_schema_%s_total_size", Name: "table_size"},
		},
	}
	tableBloatSizeChartTmpl = collectorapi.Chart{
		ID:       "table_%s_db_%s_schema_%s_bloat_size",
		Title:    "Table bloat size",
		Units:    "B",
		Fam:      "bloat",
		Ctx:      "postgres.table_bloat_size",
		Priority: prioTableBloatSize,
		Dims: collectorapi.Dims{
			{ID: "table_%s_db_%s_schema_%s_bloat_size", Name: "bloat"},
		},
	}
)

func newTableCharts(tbl *tableMetrics) *collectorapi.Charts {
	charts := tableChartsTmpl.Copy()

	if tbl.bloatSize == nil {
		_ = charts.Remove(tableBloatSizeChartTmpl.ID)
		_ = charts.Remove(tableBloatSizePercChartTmpl.ID)
	}

	for i, chart := range *charts {
		(*charts)[i] = newTableChart(chart, tbl)
	}

	return charts
}

func newTableChart(chart *collectorapi.Chart, tbl *tableMetrics) *collectorapi.Chart {
	chart = chart.Copy()
	chart.ID = fmt.Sprintf(chart.ID, tbl.name, tbl.db, tbl.schema)
	chart.Labels = []collectorapi.Label{
		{Key: "database", Value: tbl.db},
		{Key: "schema", Value: tbl.schema},
		{Key: "table", Value: tbl.name},
		{Key: "parent_table", Value: tbl.parentName},
	}
	for _, d := range chart.Dims {
		d.ID = fmt.Sprintf(d.ID, tbl.name, tbl.db, tbl.schema)
	}
	for _, v := range chart.Vars {
		v.ID = fmt.Sprintf(v.ID, tbl.name, tbl.db, tbl.schema)
	}
	return chart
}

func (c *Collector) addNewTableCharts(tbl *tableMetrics) {
	charts := newTableCharts(tbl)
	if err := c.Charts().Add(*charts...); err != nil {
		c.Warning(err)
	}
}

func (c *Collector) addTableLastAutoVacuumAgoChart(tbl *tableMetrics) {
	chart := newTableChart(tableAutoVacuumSinceTimeChartTmpl.Copy(), tbl)

	if err := c.Charts().Add(chart); err != nil {
		c.Warning(err)
	}
}

func (c *Collector) addTableLastVacuumAgoChart(tbl *tableMetrics) {
	chart := newTableChart(tableVacuumSinceTimeChartTmpl.Copy(), tbl)

	if err := c.Charts().Add(chart); err != nil {
		c.Warning(err)
	}
}

func (c *Collector) addTableLastAutoAnalyzeAgoChart(tbl *tableMetrics) {
	chart := newTableChart(tableAutoAnalyzeSinceTimeChartTmpl.Copy(), tbl)

	if err := c.Charts().Add(chart); err != nil {
		c.Warning(err)
	}
}

func (c *Collector) addTableLastAnalyzeAgoChart(tbl *tableMetrics) {
	chart := newTableChart(tableAnalyzeSinceTimeChartTmpl.Copy(), tbl)

	if err := c.Charts().Add(chart); err != nil {
		c.Warning(err)
	}
}

func (c *Collector) addTableIOChartsCharts(tbl *tableMetrics) {
	charts := collectorapi.Charts{
		newTableChart(tableCacheIORatioChartTmpl.Copy(), tbl),
		newTableChart(tableIORateChartTmpl.Copy(), tbl),
	}

	if err := c.Charts().Add(charts...); err != nil {
		c.Warning(err)
	}
}

func (c *Collector) addTableIndexIOCharts(tbl *tableMetrics) {
	charts := collectorapi.Charts{
		newTableChart(tableIndexCacheIORatioChartTmpl.Copy(), tbl),
		newTableChart(tableIndexIORateChartTmpl.Copy(), tbl),
	}

	if err := c.Charts().Add(charts...); err != nil {
		c.Warning(err)
	}
}

func (c *Collector) addTableTOASTIOCharts(tbl *tableMetrics) {
	charts := collectorapi.Charts{
		newTableChart(tableTOASCacheIORatioChartTmpl.Copy(), tbl),
		newTableChart(tableTOASTIORateChartTmpl.Copy(), tbl),
	}

	if err := c.Charts().Add(charts...); err != nil {
		c.Warning(err)
	}
}

func (c *Collector) addTableTOASTIndexIOCharts(tbl *tableMetrics) {
	charts := collectorapi.Charts{
		newTableChart(tableTOASTIndexCacheIORatioChartTmpl.Copy(), tbl),
		newTableChart(tableTOASTIndexIORateChartTmpl.Copy(), tbl),
	}

	if err := c.Charts().Add(charts...); err != nil {
		c.Warning(err)
	}
}

func (c *Collector) removeTableCharts(tbl *tableMetrics) {
	prefix := fmt.Sprintf("table_%s_db_%s_schema_%s", tbl.name, tbl.db, tbl.schema)
	for _, c := range *c.Charts() {
		if strings.HasPrefix(c.ID, prefix) {
			c.MarkRemove()
			c.MarkNotCreated()
		}
	}
}

var (
	indexChartsTmpl = collectorapi.Charts{
		indexSizeChartTmpl.Copy(),
		indexBloatSizePercChartTmpl.Copy(),
		indexBloatSizeChartTmpl.Copy(),
		indexUsageStatusChartTmpl.Copy(),
	}
	indexSizeChartTmpl = collectorapi.Chart{
		ID:       "index_%s_table_%s_db_%s_schema_%s_size",
		Title:    "Index size",
		Units:    "B",
		Fam:      "size",
		Ctx:      "postgres.index_size",
		Priority: prioIndexSize,
		Dims: collectorapi.Dims{
			{ID: "index_%s_table_%s_db_%s_schema_%s_size", Name: "size"},
		},
	}
	indexBloatSizePercChartTmpl = collectorapi.Chart{
		ID:       "index_%s_table_%s_db_%s_schema_%s_bloat_size_perc",
		Title:    "Index bloat size percentage",
		Units:    "percentage",
		Fam:      "bloat",
		Ctx:      "postgres.index_bloat_size_perc",
		Priority: prioIndexBloatSizePerc,
		Dims: collectorapi.Dims{
			{ID: "index_%s_table_%s_db_%s_schema_%s_bloat_size_perc", Name: "bloat"},
		},
		Vars: collectorapi.Vars{
			{ID: "index_%s_table_%s_db_%s_schema_%s_size", Name: "index_size"},
		},
	}
	indexBloatSizeChartTmpl = collectorapi.Chart{
		ID:       "index_%s_table_%s_db_%s_schema_%s_bloat_size",
		Title:    "Index bloat size",
		Units:    "B",
		Fam:      "bloat",
		Ctx:      "postgres.index_bloat_size",
		Priority: prioIndexBloatSize,
		Dims: collectorapi.Dims{
			{ID: "index_%s_table_%s_db_%s_schema_%s_bloat_size", Name: "bloat"},
		},
	}
	indexUsageStatusChartTmpl = collectorapi.Chart{
		ID:       "index_%s_table_%s_db_%s_schema_%s_usage_status",
		Title:    "Index usage status",
		Units:    "status",
		Fam:      "maintenance",
		Ctx:      "postgres.index_usage_status",
		Priority: prioIndexUsageStatus,
		Dims: collectorapi.Dims{
			{ID: "index_%s_table_%s_db_%s_schema_%s_usage_status_used", Name: "used"},
			{ID: "index_%s_table_%s_db_%s_schema_%s_usage_status_unused", Name: "unused"},
		},
	}
)

func (c *Collector) addNewIndexCharts(idx *indexMetrics) {
	charts := indexChartsTmpl.Copy()

	if idx.bloatSize == nil {
		_ = charts.Remove(indexBloatSizeChartTmpl.ID)
		_ = charts.Remove(indexBloatSizePercChartTmpl.ID)
	}

	for _, chart := range *charts {
		chart.ID = fmt.Sprintf(chart.ID, idx.name, idx.table, idx.db, idx.schema)
		chart.Labels = []collectorapi.Label{
			{Key: "database", Value: idx.db},
			{Key: "schema", Value: idx.schema},
			{Key: "table", Value: idx.table},
			{Key: "parent_table", Value: idx.parentTable},
			{Key: "index", Value: idx.name},
		}
		for _, d := range chart.Dims {
			d.ID = fmt.Sprintf(d.ID, idx.name, idx.table, idx.db, idx.schema)
		}
		for _, v := range chart.Vars {
			v.ID = fmt.Sprintf(v.ID, idx.name, idx.table, idx.db, idx.schema)
		}
	}

	if err := c.Charts().Add(*charts...); err != nil {
		c.Warning(err)
	}
}

func (c *Collector) removeIndexCharts(idx *indexMetrics) {
	prefix := fmt.Sprintf("index_%s_table_%s_db_%s_schema_%s", idx.name, idx.table, idx.db, idx.schema)
	for _, c := range *c.Charts() {
		if strings.HasPrefix(c.ID, prefix) {
			c.MarkRemove()
			c.MarkNotCreated()
		}
	}
}
