#include <algorithm>
#include <cassert>
#include <chrono>
#include <cmath>
#include <compare>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <limits>
#include <optional>
#include <ranges>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace memory {

using Day = std::chrono::sys_days;
using namespace std::chrono_literals;

constexpr Day epoch{std::chrono::year{2026} / std::chrono::January / 1};

constexpr Day day(const std::int64_t offset) {
    return epoch + std::chrono::days{offset};
}

constexpr std::int64_t day_number(const Day value) {
    return (value - epoch).count();
}

struct ClaimId {
    std::uint32_t value{};
    auto operator<=>(const ClaimId&) const = default;
};

struct RevisionId {
    std::uint32_t value{};
    auto operator<=>(const RevisionId&) const = default;
};

struct SourceId {
    std::uint32_t value{};
    auto operator<=>(const SourceId&) const = default;
};

template <class Instant>
class HalfOpenInterval {
public:
    HalfOpenInterval(const Instant start, const std::optional<Instant> end = std::nullopt)
        : start_{start}, end_{end} {
        if (end_.has_value() && !(*end_ > start_)) {
            throw std::invalid_argument{"a half-open interval requires start < end"};
        }
    }

    [[nodiscard]] constexpr Instant start() const noexcept {
        return start_;
    }

    [[nodiscard]] constexpr std::optional<Instant> end() const noexcept {
        return end_;
    }

    [[nodiscard]] constexpr bool contains(const Instant instant) const noexcept {
        return start_ <= instant && (!end_.has_value() || instant < *end_);
    }

    [[nodiscard]] constexpr bool overlaps(const HalfOpenInterval& other) const noexcept {
        const auto this_ends_before_other =
            end_.has_value() && *end_ <= other.start_;
        const auto other_ends_before_this =
            other.end_.has_value() && *other.end_ <= start_;
        return !this_ends_before_other && !other_ends_before_this;
    }

private:
    Instant start_;
    std::optional<Instant> end_;
};

using DayInterval = HalfOpenInterval<Day>;

enum class Polarity : std::uint8_t {
    positive,
    negative
};

enum class ClaimStatus : std::uint8_t {
    asserted,
    retracted
};

struct Source {
    SourceId id;
    std::string label;
    double authority{};
};

struct Proposition {
    std::string subject;
    std::string predicate;
    std::string object;
    std::string context;
    Polarity polarity{Polarity::positive};
};

struct ClaimVersion {
    ClaimId claim;
    RevisionId revision;
    Proposition proposition;
    Day event_time;
    DayInterval valid_time;
    DayInterval transaction_time;
    std::vector<SourceId> sources;
    ClaimStatus status{ClaimStatus::asserted};
};

struct ContradictionCandidate {
    const ClaimVersion* left{};
    const ClaimVersion* right{};
};

class BitemporalMemory {
public:
    SourceId add_source(std::string label, const double authority) {
        if (!std::isfinite(authority) || authority < 0.0 || authority > 1.0) {
            throw std::invalid_argument{"source authority must be finite and in [0, 1]"};
        }
        ensure_id_capacity<SourceId>(sources_.size());
        const auto id = SourceId{static_cast<std::uint32_t>(sources_.size())};
        sources_.push_back(Source{id, std::move(label), authority});
        return id;
    }

    ClaimId assert_claim(
        Proposition proposition,
        const Day event_time,
        DayInterval valid_time,
        const Day recorded_at,
        std::vector<SourceId> sources) {
        validate_sources(sources);
        ensure_id_capacity<ClaimId>(next_claim_);
        ensure_id_capacity<RevisionId>(versions_.size());

        const auto claim = ClaimId{static_cast<std::uint32_t>(next_claim_++)};
        const auto revision = RevisionId{static_cast<std::uint32_t>(versions_.size())};

        versions_.push_back(ClaimVersion{
            claim,
            revision,
            std::move(proposition),
            event_time,
            std::move(valid_time),
            DayInterval{recorded_at},
            std::move(sources),
            ClaimStatus::asserted
        });
        return claim;
    }

    void revise_temporal_scope(
        const ClaimId claim,
        const Day corrected_event_time,
        DayInterval corrected_valid_time,
        const Day recorded_at,
        std::vector<SourceId> sources) {
        validate_sources(sources);
        auto current = std::ranges::find_if(
            versions_,
            [claim](const ClaimVersion& version) {
                return version.claim == claim && !version.transaction_time.end().has_value();
            });
        if (current == versions_.end()) {
            throw std::invalid_argument{"claim has no open transaction-time version"};
        }
        if (!(current->transaction_time.start() < recorded_at)) {
            throw std::invalid_argument{"a revision must be recorded after its open version"};
        }

        // Closing transaction time preserves the exact interval during which
        // the previous version was visible to queries and audits.
        current->transaction_time =
            DayInterval{current->transaction_time.start(), recorded_at};

        ensure_id_capacity<RevisionId>(versions_.size());
        auto revised = *current;
        revised.revision = RevisionId{static_cast<std::uint32_t>(versions_.size())};
        revised.event_time = corrected_event_time;
        revised.valid_time = std::move(corrected_valid_time);
        revised.transaction_time = DayInterval{recorded_at};
        revised.sources = std::move(sources);
        versions_.push_back(std::move(revised));
    }

    [[nodiscard]] std::vector<const ClaimVersion*> as_of(
        const Day valid_at,
        const Day known_at) const {
        std::vector<const ClaimVersion*> result;
        result.reserve(versions_.size());

        // The two independent containment tests form a bitemporal slice.
        // Valid time asks about the modeled world; transaction time asks what
        // the memory could have known at the chosen audit instant.
        for (const auto& version : versions_) {
            if (version.status == ClaimStatus::asserted &&
                version.valid_time.contains(valid_at) &&
                version.transaction_time.contains(known_at)) {
                result.push_back(&version);
            }
        }

        std::ranges::sort(
            result,
            {},
            [](const ClaimVersion* version) {
                return std::pair{version->claim.value, version->revision.value};
            });
        return result;
    }

    [[nodiscard]] std::vector<ContradictionCandidate> contradiction_candidates(
        const std::span<const ClaimVersion* const> snapshot) const {
        std::vector<ContradictionCandidate> candidates;

        // Pairwise comparison is deliberate in this small teaching baseline.
        // Production systems index proposition signatures and temporal scopes
        // so candidate generation does not scan every pair.
        for (std::size_t left = 0; left < snapshot.size(); ++left) {
            for (std::size_t right = left + 1; right < snapshot.size(); ++right) {
                if (contradicts(*snapshot[left], *snapshot[right])) {
                    candidates.push_back({snapshot[left], snapshot[right]});
                }
            }
        }
        return candidates;
    }

    [[nodiscard]] const Source& source(const SourceId id) const {
        if (id.value >= sources_.size()) {
            throw std::out_of_range{"source identifier is outside the store"};
        }
        return sources_[id.value];
    }

    [[nodiscard]] std::span<const ClaimVersion> versions() const noexcept {
        return versions_;
    }

private:
    template <class Id>
    static void ensure_id_capacity(const std::size_t next) {
        if (next > std::numeric_limits<std::uint32_t>::max()) {
            throw std::overflow_error{"identifier space exhausted"};
        }
    }

    void validate_sources(const std::span<const SourceId> source_ids) const {
        if (source_ids.empty()) {
            throw std::invalid_argument{"every claim version requires provenance"};
        }
        for (const auto id : source_ids) {
            if (id.value >= sources_.size()) {
                throw std::invalid_argument{"claim references an unknown source"};
            }
        }
    }

    static bool contradicts(const ClaimVersion& left, const ClaimVersion& right) {
        const auto& a = left.proposition;
        const auto& b = right.proposition;

        // Opposite polarity is meaningful only after proposition identity and
        // contextual scope align. Morning coffee therefore does not contradict
        // a night-time caffeine restriction.
        return a.subject == b.subject &&
               a.predicate == b.predicate &&
               a.object == b.object &&
               a.context == b.context &&
               a.polarity != b.polarity &&
               left.valid_time.overlaps(right.valid_time);
    }

    std::vector<Source> sources_;
    std::vector<ClaimVersion> versions_;
    std::size_t next_claim_{};
};

std::string_view polarity_name(const Polarity polarity) {
    return polarity == Polarity::positive ? "positive" : "negative";
}

void print_snapshot(
    const BitemporalMemory& memory,
    const Day valid_at,
    const Day known_at) {
    const auto snapshot = memory.as_of(valid_at, known_at);
    const auto contradictions = memory.contradiction_candidates(snapshot);

    std::cout << "valid_day=" << day_number(valid_at)
              << " known_day=" << day_number(known_at)
              << " claims=" << snapshot.size()
              << " contradictions=" << contradictions.size() << '\n';

    for (const auto* version : snapshot) {
        const auto& proposition = version->proposition;
        std::cout << "  C" << version->claim.value
                  << "/R" << version->revision.value
                  << " " << polarity_name(proposition.polarity)
                  << " " << proposition.subject
                  << " " << proposition.predicate
                  << " " << proposition.object
                  << " [" << proposition.context << "]"
                  << " source=" << memory.source(version->sources.front()).label
                  << '\n';
    }
}

} // namespace memory

int main() {
    using namespace memory;

    BitemporalMemory memory;
    const auto lia_day0 = memory.add_source("Lia message on day 0", 0.95);
    const auto lia_day52 = memory.add_source("Lia message on day 52", 0.95);
    const auto lia_day86 = memory.add_source("Lia message on day 86", 0.95);
    const auto lia_day95 = memory.add_source("Lia clarification on day 95", 0.95);
    const auto clinician_note = memory.add_source("clinician note", 0.85);

    [[maybe_unused]] const auto preference = memory.assert_claim(
        {"Lia", "prefers", "tea without sugar", "general", Polarity::positive},
        day(0),
        DayInterval{day(0)},
        day(0),
        {lia_day0});

    const auto night_restriction = memory.assert_claim(
        {"Lia", "avoids", "caffeine", "night", Polarity::positive},
        day(52),
        DayInterval{day(52)},
        day(52),
        {lia_day52});

    [[maybe_unused]] const auto morning_coffee = memory.assert_claim(
        {"Lia", "drinks", "coffee", "morning", Polarity::positive},
        day(86),
        DayInterval{day(86)},
        day(86),
        {lia_day86});

    // The day 95 message changes the modeled valid past while preserving that
    // Aurora could not have known the correction during the day 90 session.
    memory.revise_temporal_scope(
        night_restriction,
        day(40),
        DayInterval{day(40)},
        day(95),
        {lia_day95});

    [[maybe_unused]] const auto claimed_end = memory.assert_claim(
        {"Lia", "avoids", "caffeine", "night", Polarity::negative},
        day(92),
        DayInterval{day(92)},
        day(98),
        {clinician_note});

    const auto before_correction = memory.as_of(day(50), day(90));
    const auto after_correction = memory.as_of(day(50), day(96));
    const auto disputed = memory.as_of(day(100), day(101));
    const auto candidates = memory.contradiction_candidates(disputed);

    assert(before_correction.size() == 1);
    assert(after_correction.size() == 2);
    assert(candidates.size() == 1);
    assert(candidates.front().left->proposition.polarity !=
           candidates.front().right->proposition.polarity);

    print_snapshot(memory, day(50), day(90));
    print_snapshot(memory, day(50), day(96));
    print_snapshot(memory, day(90), day(90));
    print_snapshot(memory, day(100), day(101));

    return 0;
}
