#pragma once

#include <algorithm>
#include <cstdint>
#include <filesystem>

#if defined(_WIN32)
#include <Windows.h>
#include <cstdio>
#include <cstring>
#include <cwchar>

namespace ta::foundation::diagnostics::crash
{
    namespace detail
    {
#if defined(_DEBUG) || defined(TA_ENABLE_CRASH_BREADCRUMBS)
        inline constexpr bool breadcrumbs_enabled = true;
#else
        inline constexpr bool breadcrumbs_enabled = false;
#endif

        constexpr std::size_t breadcrumb_header_size = 256;
        constexpr std::size_t breadcrumb_slot_size = 256;
        constexpr std::size_t breadcrumb_slot_count = 1024;
        constexpr std::size_t breadcrumb_file_size =
            breadcrumb_header_size + breadcrumb_slot_size * breadcrumb_slot_count;

        inline wchar_t log_path[2048]{};
        inline PVOID vectored_handler_handle = nullptr;
        inline std::uintptr_t module_begin = 0;
        inline std::uintptr_t module_end = 0;
        inline volatile LONG exception_sequence = 0;

        inline HANDLE breadcrumb_file = INVALID_HANDLE_VALUE;
        inline HANDLE breadcrumb_mapping = nullptr;
        inline char* breadcrumb_view = nullptr;
        inline volatile LONG64 breadcrumb_sequence = 0;

        inline thread_local const char* current_stage = "unassigned";
        inline thread_local std::uintptr_t context0 = 0;
        inline thread_local std::uintptr_t context1 = 0;
        inline thread_local std::uintptr_t context2 = 0;
        inline thread_local bool callback_active = false;
        inline thread_local bool in_raid = false;
        inline thread_local bool handling_exception = false;

        inline bool is_fatal_code(const DWORD code) noexcept
        {
            switch (code)
            {
            case EXCEPTION_ACCESS_VIOLATION:
            case EXCEPTION_IN_PAGE_ERROR:
            case EXCEPTION_ILLEGAL_INSTRUCTION:
            case EXCEPTION_PRIV_INSTRUCTION:
            case EXCEPTION_STACK_OVERFLOW:
            case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
            case EXCEPTION_DATATYPE_MISALIGNMENT:
            case 0xC0000374UL: // heap corruption
            case 0xC0000409UL: // stack buffer overrun / fast-fail status
                return true;
            default:
                return false;
            }
        }

        inline bool address_in_module(const void* address) noexcept
        {
            const auto value = reinterpret_cast<std::uintptr_t>(address);
            return module_begin != 0 && value >= module_begin && value < module_end;
        }

        inline void initialize_module_range(HMODULE module) noexcept
        {
            module_begin = reinterpret_cast<std::uintptr_t>(module);
            module_end = module_begin;

            const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(module);
            if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE)
                return;

            const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(
                reinterpret_cast<const std::uint8_t*>(module) + dos->e_lfanew);
            if (nt && nt->Signature == IMAGE_NT_SIGNATURE)
                module_end = module_begin + nt->OptionalHeader.SizeOfImage;
        }

        inline void close_breadcrumbs() noexcept
        {
            if (breadcrumb_view)
            {
                static_cast<void>(FlushViewOfFile(breadcrumb_view, 0));
                UnmapViewOfFile(breadcrumb_view);
                breadcrumb_view = nullptr;
            }
            if (breadcrumb_mapping)
            {
                CloseHandle(breadcrumb_mapping);
                breadcrumb_mapping = nullptr;
            }
            if (breadcrumb_file != INVALID_HANDLE_VALUE)
            {
                CloseHandle(breadcrumb_file);
                breadcrumb_file = INVALID_HANDLE_VALUE;
            }
            breadcrumb_sequence = 0;
        }

        inline bool open_breadcrumbs(const std::filesystem::path& path) noexcept
        {
            close_breadcrumbs();
            breadcrumb_file = CreateFileW(
                path.c_str(),
                GENERIC_READ | GENERIC_WRITE,
                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                nullptr,
                CREATE_ALWAYS,
                FILE_ATTRIBUTE_NORMAL,
                nullptr);
            if (breadcrumb_file == INVALID_HANDLE_VALUE)
                return false;

            LARGE_INTEGER size{};
            size.QuadPart = static_cast<LONGLONG>(breadcrumb_file_size);
            if (!SetFilePointerEx(breadcrumb_file, size, nullptr, FILE_BEGIN) ||
                !SetEndOfFile(breadcrumb_file))
            {
                close_breadcrumbs();
                return false;
            }

            breadcrumb_mapping = CreateFileMappingW(
                breadcrumb_file,
                nullptr,
                PAGE_READWRITE,
                0,
                0,
                nullptr);
            if (!breadcrumb_mapping)
            {
                close_breadcrumbs();
                return false;
            }

            breadcrumb_view = static_cast<char*>(MapViewOfFile(
                breadcrumb_mapping,
                FILE_MAP_ALL_ACCESS,
                0,
                0,
                breadcrumb_file_size));
            if (!breadcrumb_view)
            {
                close_breadcrumbs();
                return false;
            }

            std::memset(breadcrumb_view, ' ', breadcrumb_file_size);
            constexpr char header[] =
                "===== circular crash breadcrumbs; sort records by seq; newest records overwrite oldest =====";
            std::memcpy(
                breadcrumb_view,
                header,
                (std::min)(sizeof(header) - 1, breadcrumb_header_size - 2));
            breadcrumb_view[breadcrumb_header_size - 2] = '\r';
            breadcrumb_view[breadcrumb_header_size - 1] = '\n';
            for (std::size_t index = 0; index < breadcrumb_slot_count; ++index)
            {
                char* slot = breadcrumb_view + breadcrumb_header_size + index * breadcrumb_slot_size;
                slot[breadcrumb_slot_size - 2] = '\r';
                slot[breadcrumb_slot_size - 1] = '\n';
            }
            static_cast<void>(FlushViewOfFile(breadcrumb_view, 0));
            return true;
        }

        inline void write_breadcrumb(const char* stage,
                                     const std::uintptr_t value0,
                                     const std::uintptr_t value1,
                                     const std::uintptr_t value2) noexcept
        {
            if (!breadcrumb_view)
                return;

            const LONG64 sequence = InterlockedIncrement64(&breadcrumb_sequence);
            const std::size_t index = static_cast<std::size_t>(
                (sequence - 1) % static_cast<LONG64>(breadcrumb_slot_count));
            char* slot = breadcrumb_view + breadcrumb_header_size + index * breadcrumb_slot_size;

            char text[breadcrumb_slot_size]{};
            const int length = _snprintf_s(
                text,
                sizeof(text),
                _TRUNCATE,
                "seq=%020lld tick=%012llu tid=%010lu callback=%u in_raid=%u stage=%s ctx0=0x%016llX ctx1=0x%016llX ctx2=0x%016llX",
                static_cast<long long>(sequence),
                static_cast<unsigned long long>(GetTickCount64()),
                GetCurrentThreadId(),
                callback_active ? 1U : 0U,
                in_raid ? 1U : 0U,
                stage ? stage : "null",
                static_cast<unsigned long long>(value0),
                static_cast<unsigned long long>(value1),
                static_cast<unsigned long long>(value2));

            std::memset(slot, ' ', breadcrumb_slot_size);
            if (length > 0)
            {
                const std::size_t copy_length = (std::min)(
                    static_cast<std::size_t>(length),
                    breadcrumb_slot_size - 2);
                std::memcpy(slot, text, copy_length);
            }
            slot[breadcrumb_slot_size - 2] = '\r';
            slot[breadcrumb_slot_size - 1] = '\n';

            // Periodic persistence keeps overhead low while retaining useful
            // context even when the process terminates without normal cleanup.
            if ((sequence & 0xFF) == 0)
                static_cast<void>(FlushViewOfFile(breadcrumb_view, 0));
        }

        inline void append_bytes(const char* bytes, const DWORD length) noexcept
        {
            if (!bytes || length == 0 || log_path[0] == L'\0')
                return;

            HANDLE file = CreateFileW(
                log_path,
                FILE_APPEND_DATA,
                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                nullptr,
                OPEN_ALWAYS,
                FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH,
                nullptr);
            if (file == INVALID_HANDLE_VALUE)
                return;

            DWORD written = 0;
            static_cast<void>(WriteFile(file, bytes, length, &written, nullptr));
            static_cast<void>(FlushFileBuffers(file));
            CloseHandle(file);
        }

        inline const char* access_kind(const ULONG_PTR operation) noexcept
        {
            switch (operation)
            {
            case 0: return "read";
            case 1: return "write";
            case 8: return "execute";
            default: return "unknown";
            }
        }

        inline LONG CALLBACK vectored_exception_handler(EXCEPTION_POINTERS* pointers) noexcept
        {
            if (!pointers || !pointers->ExceptionRecord || !pointers->ContextRecord)
                return EXCEPTION_CONTINUE_SEARCH;

            const EXCEPTION_RECORD* record = pointers->ExceptionRecord;
            if (!is_fatal_code(record->ExceptionCode))
                return EXCEPTION_CONTINUE_SEARCH;

            if (!callback_active && !address_in_module(record->ExceptionAddress))
                return EXCEPTION_CONTINUE_SEARCH;

            if (handling_exception)
                return EXCEPTION_CONTINUE_SEARCH;
            handling_exception = true;

            const LONG event_sequence = InterlockedIncrement(&exception_sequence);
            SYSTEMTIME time{};
            GetLocalTime(&time);

            ULONG_PTR operation = static_cast<ULONG_PTR>(-1);
            ULONG_PTR fault_address = 0;
            if ((record->ExceptionCode == EXCEPTION_ACCESS_VIOLATION ||
                 record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR) &&
                record->NumberParameters >= 2)
            {
                operation = record->ExceptionInformation[0];
                fault_address = record->ExceptionInformation[1];
            }

            const auto exception_address = reinterpret_cast<std::uintptr_t>(record->ExceptionAddress);
            const auto module_offset = address_in_module(record->ExceptionAddress)
                ? exception_address - module_begin
                : 0;

            char line[4096]{};
#if defined(_WIN64)
            const CONTEXT* context = pointers->ContextRecord;
            const int length = _snprintf_s(
                line,
                sizeof(line),
                _TRUNCATE,
                "%04u-%02u-%02u %02u:%02u:%02u.%03u [EXCEPTION] seq=%ld tid=%lu code=0x%08lX exception=0x%llX module_offset=0x%llX stage=%s callback=%u in_raid=%u ctx0=0x%llX ctx1=0x%llX ctx2=0x%llX access=%s fault=0x%llX rip=0x%llX rsp=0x%llX rbp=0x%llX rax=0x%llX rbx=0x%llX rcx=0x%llX rdx=0x%llX rsi=0x%llX rdi=0x%llX r8=0x%llX r9=0x%llX r10=0x%llX r11=0x%llX\r\n",
                time.wYear, time.wMonth, time.wDay,
                time.wHour, time.wMinute, time.wSecond, time.wMilliseconds,
                event_sequence,
                GetCurrentThreadId(),
                record->ExceptionCode,
                static_cast<unsigned long long>(exception_address),
                static_cast<unsigned long long>(module_offset),
                current_stage ? current_stage : "null",
                callback_active ? 1U : 0U,
                in_raid ? 1U : 0U,
                static_cast<unsigned long long>(context0),
                static_cast<unsigned long long>(context1),
                static_cast<unsigned long long>(context2),
                operation == static_cast<ULONG_PTR>(-1) ? "n/a" : access_kind(operation),
                static_cast<unsigned long long>(fault_address),
                static_cast<unsigned long long>(context->Rip),
                static_cast<unsigned long long>(context->Rsp),
                static_cast<unsigned long long>(context->Rbp),
                static_cast<unsigned long long>(context->Rax),
                static_cast<unsigned long long>(context->Rbx),
                static_cast<unsigned long long>(context->Rcx),
                static_cast<unsigned long long>(context->Rdx),
                static_cast<unsigned long long>(context->Rsi),
                static_cast<unsigned long long>(context->Rdi),
                static_cast<unsigned long long>(context->R8),
                static_cast<unsigned long long>(context->R9),
                static_cast<unsigned long long>(context->R10),
                static_cast<unsigned long long>(context->R11));
#else
            const CONTEXT* context = pointers->ContextRecord;
            const int length = _snprintf_s(
                line,
                sizeof(line),
                _TRUNCATE,
                "%04u-%02u-%02u %02u:%02u:%02u.%03u [EXCEPTION] seq=%ld tid=%lu code=0x%08lX exception=0x%lX module_offset=0x%lX stage=%s callback=%u in_raid=%u ctx0=0x%lX ctx1=0x%lX ctx2=0x%lX access=%s fault=0x%lX eip=0x%lX esp=0x%lX ebp=0x%lX\r\n",
                time.wYear, time.wMonth, time.wDay,
                time.wHour, time.wMinute, time.wSecond, time.wMilliseconds,
                event_sequence,
                GetCurrentThreadId(),
                record->ExceptionCode,
                static_cast<unsigned long>(exception_address),
                static_cast<unsigned long>(module_offset),
                current_stage ? current_stage : "null",
                callback_active ? 1U : 0U,
                in_raid ? 1U : 0U,
                static_cast<unsigned long>(context0),
                static_cast<unsigned long>(context1),
                static_cast<unsigned long>(context2),
                operation == static_cast<ULONG_PTR>(-1) ? "n/a" : access_kind(operation),
                static_cast<unsigned long>(fault_address),
                context->Eip,
                context->Esp,
                context->Ebp);
#endif
            if (length > 0)
                append_bytes(line, static_cast<DWORD>(length));

            write_breadcrumb(
                "exception",
                exception_address,
                fault_address,
                static_cast<std::uintptr_t>(record->ExceptionCode));
            if (breadcrumb_view)
                static_cast<void>(FlushViewOfFile(breadcrumb_view, 0));

            handling_exception = false;
            return EXCEPTION_CONTINUE_SEARCH;
        }
    }

    inline void set_stage(const char* stage,
                          const std::uintptr_t value0 = 0,
                          const std::uintptr_t value1 = 0,
                          const std::uintptr_t value2 = 0) noexcept
    {
        detail::current_stage = stage ? stage : "null";
        detail::context0 = value0;
        detail::context1 = value1;
        detail::context2 = value2;
        if constexpr (detail::breadcrumbs_enabled)
            detail::write_breadcrumb(detail::current_stage, value0, value1, value2);
    }

    inline void enter_callback(const char* stage,
                               const std::uintptr_t value0 = 0,
                               const std::uintptr_t value1 = 0,
                               const std::uintptr_t value2 = 0) noexcept
    {
        detail::callback_active = true;
        set_stage(stage, value0, value1, value2);
    }

    inline void leave_callback(const char* stage = "callback.complete") noexcept
    {
        set_stage(stage);
        detail::callback_active = false;
    }

    class CallbackScope final
    {
    public:
        CallbackScope(const char* stage,
                      const std::uintptr_t value0 = 0,
                      const std::uintptr_t value1 = 0,
                      const std::uintptr_t value2 = 0) noexcept
        {
            enter_callback(stage, value0, value1, value2);
        }

        ~CallbackScope() noexcept
        {
            leave_callback();
        }

        CallbackScope(const CallbackScope&) = delete;
        CallbackScope& operator=(const CallbackScope&) = delete;
    };

    inline void set_in_raid(const bool value) noexcept
    {
        detail::in_raid = value;
    }

    inline void write_event(const char* event,
                            const std::uintptr_t value0 = 0,
                            const std::uintptr_t value1 = 0,
                            const std::uintptr_t value2 = 0) noexcept
    {
        SYSTEMTIME time{};
        GetLocalTime(&time);
        char line[1024]{};
        const int length = _snprintf_s(
            line,
            sizeof(line),
            _TRUNCATE,
            "%04u-%02u-%02u %02u:%02u:%02u.%03u [EVENT] tid=%lu event=%s stage=%s in_raid=%u value0=0x%llX value1=0x%llX value2=0x%llX\r\n",
            time.wYear, time.wMonth, time.wDay,
            time.wHour, time.wMinute, time.wSecond, time.wMilliseconds,
            GetCurrentThreadId(),
            event ? event : "null",
            detail::current_stage ? detail::current_stage : "null",
            detail::in_raid ? 1U : 0U,
            static_cast<unsigned long long>(value0),
            static_cast<unsigned long long>(value1),
            static_cast<unsigned long long>(value2));
        if (length > 0)
            detail::append_bytes(line, static_cast<DWORD>(length));
        if constexpr (detail::breadcrumbs_enabled)
        {
            detail::write_breadcrumb(event ? event : "event", value0, value1, value2);
            if (detail::breadcrumb_view)
                static_cast<void>(FlushViewOfFile(detail::breadcrumb_view, 0));
        }
    }

    inline bool initialize(const std::filesystem::path& path, HMODULE module)
    {
        if (path.empty() || !module)
            return false;

        const wchar_t* source = path.c_str();
        if (!source || wcsncpy_s(
                detail::log_path,
                _countof(detail::log_path),
                source,
                _TRUNCATE) != 0)
        {
            return false;
        }

        detail::initialize_module_range(module);

        HANDLE file = CreateFileW(
            detail::log_path,
            GENERIC_WRITE,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            nullptr,
            CREATE_ALWAYS,
            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH,
            nullptr);
        if (file == INVALID_HANDLE_VALUE)
        {
            detail::log_path[0] = L'\0';
            return false;
        }

        constexpr char header[] = "===== crash diagnostic session =====\r\n";
        DWORD written = 0;
        static_cast<void>(WriteFile(file, header, static_cast<DWORD>(sizeof(header) - 1), &written, nullptr));
        static_cast<void>(FlushFileBuffers(file));
        CloseHandle(file);

        if constexpr (detail::breadcrumbs_enabled)
        {
            const std::wstring filename = path.filename().wstring();
            constexpr wchar_t crash_prefix[] = L"crash_diagnostics_";
            constexpr wchar_t log_extension[] = L".log";
            constexpr std::size_t crash_prefix_length =
                sizeof(crash_prefix) / sizeof(crash_prefix[0]) - 1;
            constexpr std::size_t log_extension_length =
                sizeof(log_extension) / sizeof(log_extension[0]) - 1;
            std::filesystem::path breadcrumb_path = path.parent_path() / L"crash_breadcrumbs.log";
            if (filename.rfind(crash_prefix, 0) == 0 &&
                filename.size() > crash_prefix_length + log_extension_length &&
                filename.compare(
                    filename.size() - log_extension_length,
                    log_extension_length,
                    log_extension) == 0)
            {
                const std::size_t timestamp_begin = crash_prefix_length;
                const std::size_t timestamp_length =
                    filename.size() - timestamp_begin - log_extension_length;
                breadcrumb_path = path.parent_path() /
                    (L"crash_breadcrumbs_" +
                     filename.substr(timestamp_begin, timestamp_length) +
                     L".log");
            }
            static_cast<void>(detail::open_breadcrumbs(breadcrumb_path));
        }

        detail::vectored_handler_handle = AddVectoredExceptionHandler(
            1,
            detail::vectored_exception_handler);
        if (!detail::vectored_handler_handle)
        {
            detail::close_breadcrumbs();
            detail::log_path[0] = L'\0';
            return false;
        }

        write_event("diagnostics-initialized", detail::module_begin, detail::module_end, 0);
        return true;
    }

    inline void shutdown() noexcept
    {
        write_event("diagnostics-shutdown");
        if (detail::vectored_handler_handle)
        {
            RemoveVectoredExceptionHandler(detail::vectored_handler_handle);
            detail::vectored_handler_handle = nullptr;
        }
        detail::close_breadcrumbs();
        detail::log_path[0] = L'\0';
        detail::module_begin = 0;
        detail::module_end = 0;
    }
}

#else

namespace ta::foundation::diagnostics::crash
{
    inline void set_stage(const char*, std::uintptr_t = 0, std::uintptr_t = 0, std::uintptr_t = 0) noexcept {}
    inline void enter_callback(const char*, std::uintptr_t = 0, std::uintptr_t = 0, std::uintptr_t = 0) noexcept {}
    inline void leave_callback(const char* = "callback.complete") noexcept {}
    class CallbackScope final
    {
    public:
        CallbackScope(const char*, std::uintptr_t = 0, std::uintptr_t = 0, std::uintptr_t = 0) noexcept {}
    };
    inline void set_in_raid(bool) noexcept {}
    inline void write_event(const char*, std::uintptr_t = 0, std::uintptr_t = 0, std::uintptr_t = 0) noexcept {}
    inline bool initialize(const std::filesystem::path&, void*) { return false; }
    inline void shutdown() noexcept {}
}

#endif
