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

#include "common.h"

static bool svc_rrddim_obsolete_to_archive(RRDDIM *rd) {
    RRDSET *st = rd->rrdset;

    if(rrddim_flag_check(rd, RRDDIM_FLAG_OBSOLETE) && spinlock_trylock(&rd->destroy_lock)) {
        if(!rrddim_flag_check(rd, RRDDIM_FLAG_OBSOLETE)) {
            spinlock_unlock(&rd->destroy_lock);
            return false;
        }
    }
    else
        return false;

    worker_is_busy(UV_EVENT_ARCHIVE_DIMENSION);

    if (rd->rrd_memory_mode == RRD_DB_MODE_DBENGINE) {
        if (!rrddim_finalize_collection_and_check_retention(rd)) {
            /* This metric has no data and no references */
            metaqueue_delete_dimension_uuid(uuidmap_uuid_ptr(rd->uuid));
        }
    }

    worker_is_busy(UV_EVENT_FREE_DIMENSION);
    rrddim_free(st, rd);
    return true;
}

// Returns the number of dimensions actually archived this call.
//
// Two callable shapes:
//   1. all_dimensions == false and RRDSET_FLAG_OBSOLETE_DIMENSIONS unset:
//      early-return path. Nothing scanned, flag not touched, returns 0.
//   2. Any other case: scans the dimensions. The flag is cleared up
//      front, then re-set at the end iff some candidate could not be
//      archived this pass. Callers on this path can detect "all
//      candidates archived" by reading the flag after the call.
static inline size_t svc_rrdset_archive_obsolete_dimensions(RRDSET *st, bool all_dimensions) {
    if(!all_dimensions && !rrdset_flag_check(st, RRDSET_FLAG_OBSOLETE_DIMENSIONS))
        return 0;

    worker_is_busy(UV_EVENT_ARCHIVE_CHART_DIMENSIONS);

    rrdset_flag_clear(st, RRDSET_FLAG_OBSOLETE_DIMENSIONS);

    RRDDIM *rd;
    time_t now = now_realtime_sec();

    size_t dim_candidates = 0;
    size_t dim_archives = 0;

    dfe_start_write(st->rrddim_root_index, rd) {
        bool candidate = (all_dimensions || rrddim_flag_check(rd, RRDDIM_FLAG_OBSOLETE));

        if(candidate) {
            dim_candidates++;

            if(rd->collector.last_collected_time.tv_sec + rrdset_free_obsolete_time_s < now) {
                size_t references = dictionary_acquired_item_references(rd_dfe.item);
                if(references == 1) {
                    if(svc_rrddim_obsolete_to_archive(rd))
                        dim_archives++;
                }
            }
        }
    }
    dfe_done(rd);

    if(dim_archives != dim_candidates)
        rrdset_flag_set(st, RRDSET_FLAG_OBSOLETE_DIMENSIONS);

    return dim_archives;
}

static bool svc_rrdset_lock_for_deletion(RRDSET *st, time_t now) {
    if(st->last_accessed_time_s + rrdset_free_obsolete_time_s < now &&
        st->last_updated.tv_sec + rrdset_free_obsolete_time_s < now &&
        st->last_collected_time.tv_sec + rrdset_free_obsolete_time_s < now &&
        spinlock_trylock(&st->destroy_lock)) {

        if(rrdset_flag_check(st, RRDSET_FLAG_OBSOLETE))
            return true;

        spinlock_unlock(&st->destroy_lock);
    }

    return false;
}

static inline size_t svc_rrdhost_cleanup_charts_marked_obsolete(RRDHOST *host) {
    if(!rrdhost_flag_check(host, RRDHOST_FLAG_PENDING_OBSOLETE_CHARTS|RRDHOST_FLAG_PENDING_OBSOLETE_DIMENSIONS))
        return 0;

    worker_is_busy(UV_EVENT_CLEANUP_OBSOLETE_CHARTS);

    rrdhost_flag_clear(host, RRDHOST_FLAG_PENDING_OBSOLETE_CHARTS|RRDHOST_FLAG_PENDING_OBSOLETE_DIMENSIONS);

    size_t full_candidates = 0;
    size_t full_archives = 0;
    size_t partial_candidates = 0;
    size_t partial_archives = 0;
    // Total archived metadata items (RRDMETRIC + RRDINSTANCE). Used by the
    // caller to decide whether to schedule a deep rrdcontext GC pass.
    // Counts every dimension archived (each produces an archived RRDMETRIC)
    // plus every chart freed (each produces an archived RRDINSTANCE).
    size_t archived_items = 0;

    time_t now = now_realtime_sec();
    RRDSET *st;
    rrdset_foreach_reentrant(st, host) {
        if(rrdset_is_replicating(st))
            continue;

        RRDSET_FLAGS flags = rrdset_flag_get(st);

        if(flags & RRDSET_FLAG_OBSOLETE_DIMENSIONS) {
            partial_candidates++;

            archived_items += svc_rrdset_archive_obsolete_dimensions(st, false);

            // "all candidates archived" -> flag was not re-set inside.
            if(!rrdset_flag_check(st, RRDSET_FLAG_OBSOLETE_DIMENSIONS))
                partial_archives++;
        }

        if(flags & RRDSET_FLAG_OBSOLETE) {
            full_candidates++;

            if(svc_rrdset_lock_for_deletion(st, now)) {
                archived_items += svc_rrdset_archive_obsolete_dimensions(st, true);

                if(!rrdset_flag_check(st, RRDSET_FLAG_OBSOLETE_DIMENSIONS)) {
                    full_archives++;
                    archived_items++;       // rrdset_free archives the RRDINSTANCE

                    worker_is_busy(UV_EVENT_FREE_CHART);
                    rrdset_free(st);
                }
                else
                    spinlock_unlock(&st->destroy_lock);
            }
        }
    }
    rrdset_foreach_done(st);

    dictionary_garbage_collect(host->rrdset_root_index);

    if(partial_archives != partial_candidates)
        rrdhost_flag_set(host, RRDHOST_FLAG_PENDING_OBSOLETE_DIMENSIONS);

    if(full_archives != full_candidates)
        rrdhost_flag_set(host, RRDHOST_FLAG_PENDING_OBSOLETE_CHARTS);

    return archived_items;
}

void svc_rrdhost_obsolete_all_charts(RRDHOST *host) {
    ml_host_disconnected(host);

    RRDSET *st;
    rrdset_foreach_read(st, host) {
        rrdset_is_obsolete___safe_from_collector_thread(st);
    }
    rrdset_foreach_done(st);
}

static void svc_rrd_cleanup_obsolete_charts_from_all_hosts() {
    worker_is_busy(UV_EVENT_CLEANUP_OBSOLETE_CHARTS_ON_HOSTS);

    rrd_rdlock();

    size_t archived = 0;

    RRDHOST *host;
    rrdhost_foreach_read(host) {
        if(rrdhost_receiver_replicating_charts(host) || rrdhost_sender_replicating_charts(host))
            continue;

        archived += svc_rrdhost_cleanup_charts_marked_obsolete(host);

        if (rrdhost_is_local(host) || IS_VIRTUAL_HOST_OS(host))
            continue;

        rrdhost_receiver_lock(host);

        time_t now = now_realtime_sec();

        if (!host->receiver &&
            host->stream.rcv.status.last_connected == 0 &&
            (host->stream.rcv.status.last_disconnected + rrdset_free_obsolete_time_s < now)) {
            svc_rrdhost_obsolete_all_charts(host);
        }

        rrdhost_receiver_unlock(host);
    }

    rrd_rdunlock();

    // If anything was archived (a chart freed, or just dimensions archived
    // on a still-live chart), schedule a deep rrdcontext GC pass. On
    // non-dbengine hosts, dbengine rotation never triggers it, so archived
    // RRDINSTANCE / RRDMETRIC entries would otherwise accumulate forever in
    // host->rrdctx.contexts -> rc->rrdinstances / ri->rrdmetrics.
    if(archived)
        rrdcontext_request_full_gc();
}

static void svc_rrdhost_cleanup_orphan_hosts(RRDHOST *protected_host) {
    worker_is_busy(UV_EVENT_CLEANUP_ORPHAN_HOSTS);

    time_t now = now_realtime_sec();

    rrd_wrlock();
    RRDHOST *host, *next = localhost;
    while((host = next) != NULL) {
        next = host->next;

        if(!rrdhost_should_be_cleaned_up(host, protected_host, now))
            continue;

        bool delete = rrdhost_free_ephemeral_time_s &&
                      now - host->stream.rcv.status.last_disconnected > rrdhost_free_ephemeral_time_s &&
                      rrdhost_option_check(host, RRDHOST_OPTION_EPHEMERAL_HOST);

        if (!delete && rrdhost_flag_check(host, RRDHOST_FLAG_ARCHIVED)) {
            // the node is archived, so the cleanup has already run
            // however, the node may not have any retention now
            // so it may still need to be needed
            time_t from_s = 0, to_s = 0;
            rrdhost_retention(host, now, rrdhost_is_online(host), &from_s, &to_s);
            if(!from_s && !to_s)
                delete = true;
            else
                continue;
        }

        worker_is_busy(UV_EVENT_FREE_HOST);

        if (delete) {
            netdata_log_info("Host '%s' with machine guid '%s' is archived, ephemeral clean up.", rrdhost_hostname(host), host->machine_guid);

            // Save machine_guid before releasing lock - we'll use it to look up fresh pointers
            char machine_guid[UUID_STR_LEN];
            strncpyz(machine_guid, host->machine_guid, GUID_LEN);

            // Release lock before synchronous cloud operations to avoid deadlock
            // (build_node_info acquires rrd_rdlock which would deadlock with our wrlock)
            rrd_wrunlock();

            // Look up fresh pointer for cloud operations (don't use stale 'host' pointer)
            RRDHOST *cloud_host = rrdhost_find_by_guid(machine_guid);
            if (cloud_host) {
                send_node_info_with_wait(cloud_host);
                send_node_update_with_wait(cloud_host, 0, 0);
            }

            // Re-acquire lock for cleanup
            rrd_wrlock();

            // Re-validate host still exists for cleanup
            RRDHOST *host_check = rrdhost_find_by_guid(machine_guid);
            if (host_check) {
                unregister_node(host_check->machine_guid);
                rrdhost_free___while_having_rrd_wrlock(host_check);
            }

            // Restart iteration - the list may have changed while lock was released
            next = localhost;
            now = now_realtime_sec();
        }
        else
            rrdhost_cleanup_data_collection_and_health(host);
    }
    rrd_wrunlock();
}

void run_maintenace() {
    svc_rrd_cleanup_obsolete_charts_from_all_hosts();
    svc_rrdhost_cleanup_orphan_hosts(localhost);
}
