#include "server.h"

#include <cstdio>
#include <csignal>

#ifdef VOICECAT_HAS_NET

#define ASIO_STANDALONE 1
#include <asio.hpp>
#include <asio/signal_set.hpp>

#include <chrono>
#include <functional>
#include <memory>
#include <vector>

#include "conn_session.h"
#include "core/worker_pool.h"
#include "crypto/crypto.h"
#include "db.h"
#include "identity.h"
#include "media_relay.h"
#include "net/transport.h"
#include "session_registry.h"

namespace voicecat::server {

int Server::run() {
    // ── Identity + cert ──────────────────────────────────────────────────────
    ServerIdentityManager id_mgr;
    std::string error;
    if (!id_mgr.init(cfg_.data_dir, cfg_.server_name, error)) {
        std::fprintf(stderr, "[server] identity init failed: %s\n", error.c_str());
        return 1;
    }

    // ── Database + bootstrap admin ───────────────────────────────────────────
    auto db = std::make_shared<Database>(cfg_.data_dir + "/voicecat.db");
    if (!db->open(error)) {
        std::fprintf(stderr, "[server] db open failed: %s\n", error.c_str());
        return 1;
    }
    if (db->is_empty()) {
        std::string pw = Database::generate_password(20);
        auto acc = db->create_account("admin", pw, true, error);
        if (!acc) {
            std::fprintf(stderr, "[server] failed to create admin account: %s\n", error.c_str());
            return 1;
        }
        std::printf("\n");
        std::printf("┌─────────────────────────────────────────────────────────┐\n");
        std::printf("│  First run — admin account created                      │\n");
        std::printf("│  username : %-44s│\n", "admin");
        std::printf("│  password : %-44s│\n", pw.c_str());
        std::printf("│  Change with: voicecat-admin account reset admin        │\n");
        std::printf("└─────────────────────────────────────────────────────────┘\n");
        std::printf("\n");
    }

    // ── Session registry ─────────────────────────────────────────────────────
    auto registry = std::make_shared<SessionRegistry>(db);
    registry->load_channels();

    // ── Worker pool ──────────────────────────────────────────────────────────
    auto workers = std::make_shared<WorkerPool>(3);

    // ── Asio io_context ──────────────────────────────────────────────────────
    asio::io_context io;

    // ── UDP media relay (M2) ─────────────────────────────────────────────────
    auto media_relay = std::make_shared<MediaRelay>(io, registry);
    // Control and media share one port number on TCP+UDP (docs/deployment.md): when media_port
    // is left at 0, follow bind_port so a single forward rule covers both. If bind_port is also 0
    // (tests), this stays 0 and the OS picks the UDP port (reported via on_media_ready).
    uint16_t media_want = cfg_.media_port != 0 ? cfg_.media_port : cfg_.bind_port;
    if (!media_relay->bind(media_want)) {
        std::fprintf(stderr, "[server] failed to bind UDP media port %u\n", media_want);
        return 1;
    }
    media_relay->start();
    uint16_t media_bound = media_relay->media_port();
    if (cfg_.on_media_ready) cfg_.on_media_ready(media_bound);

    // Capture all locals by reference for the factory lambda (io lifetime is > factory)
    voicecat::net::TcpAcceptor acceptor(
        io, cfg_.bind_port,
        [&](asio::ip::tcp::socket sock) -> std::shared_ptr<voicecat::net::TcpServerConn> {
            auto session = std::make_shared<ConnSession>(
                db, registry, workers,
                id_mgr.identity().fingerprint,
                cfg_.allow_guests,
                media_bound);

            // Use shared_ptr (not weak_ptr) so TcpServerConn keeps ConnSession alive.
            // cycle is broken by weak_tcp in the send/close fns below.
            voicecat::net::TcpChannelCallbacks cbs;
            cbs.on_frame = [session](std::vector<uint8_t> f) {
                session->on_frame(std::move(f));
            };
            cbs.on_disconnected = [session] {
                session->on_disconnect();
            };
            cbs.on_error = [session](std::error_code) {
                session->on_disconnect();
            };
            // Derive media keys right after TLS handshake (server is not the client).
            cbs.on_tls_ready = [session](voicecat::crypto::TlsContext& tls) {
                auto send = voicecat::crypto::SodiumMediaCrypto::derive_send(tls, false);
                auto recv = voicecat::crypto::SodiumMediaCrypto::derive_recv(tls, false);
                if (send && recv) session->set_media_crypto(std::move(send), std::move(recv));
            };

            // Create a TLS context for this connection (server role).
            auto tls = std::make_unique<voicecat::crypto::TlsContext>(
                voicecat::crypto::TlsContext::Role::Server,
                &id_mgr.cert());

            auto tcp = std::make_shared<voicecat::net::TcpServerConn>(
                std::move(sock), std::move(cbs), std::move(tls));

            // Give session its send/close capability (weak_ptr avoids cycle)
            std::weak_ptr<voicecat::net::TcpServerConn> weak_tcp = tcp;
            session->set_io(
                [weak_tcp](std::vector<uint8_t> frame) {
                    if (auto t = weak_tcp.lock()) t->send_frame(std::move(frame));
                },
                [weak_tcp] {
                    if (auto t = weak_tcp.lock()) t->close();
                });

            uint64_t sid = registry->register_session(session);
            session->set_session_id(sid);
            session->begin();
            // NOTE: do NOT call tcp->start() here — TcpAcceptor::do_accept() calls it.
            return tcp;
        });

    acceptor.start();

    // Arm programmatic stop (for tests and embedders).
    {
        std::lock_guard lk(stop_mutex_);
        stop_fn_ = [&] {
            acceptor.stop();
            media_relay->stop();
            io.stop();
        };
    }

    // Notify caller of the actual bound port (matters when bind_port==0).
    uint16_t bound = acceptor.local_port();
    if (cfg_.on_ready) cfg_.on_ready(bound);

#ifndef _WIN32
    // POSIX/macOS: ignore SIGPIPE — writing to a disconnected client's TCP socket returns
    // EPIPE instead of killing the server process. On macOS SIGPIPE is delivered by
    // default (unlike Windows where the signal doesn't exist). Must be set before any
    // async writes are posted. Process-global; safe alongside the asio::signal_set below
    // (which only catches SIGINT/SIGTERM).
    std::signal(SIGPIPE, SIG_IGN);
#endif

    // Graceful shutdown on SIGINT/SIGTERM
    asio::signal_set signals(io, SIGINT, SIGTERM);
    signals.async_wait([&](std::error_code, int sig) {
        std::printf("\n[server] signal %d — shutting down\n", sig);
        acceptor.stop();
        media_relay->stop();
        io.stop();
    });

    // ── Keepalive reaper (docs/protocol.md §7) ─────────────────────────────────
    // Sweeps every reaper_sweep_ms and drops any session whose last_seen is older than
    // reaper_timeout_ms. Each close() broadcasts UserEvent::LEFT via the Tier 1 fix, so
    // peers learn about the timeout exactly like a normal disconnect — their audio engines
    // call remove_stream and stop PLC. This catches half-open connections (NAT timeout,
    // wifi loss without RST, laptop sleep) that never produce a TCP EOF and would otherwise
    // leave ghost users forever. Disabled when reaper_timeout_ms <= 0.
    asio::steady_timer reaper_timer(io);
    std::function<void()> arm_reaper;
    if (cfg_.reaper_timeout_ms > 0) {
        arm_reaper = [&] {
            reaper_timer.expires_after(std::chrono::milliseconds(cfg_.reaper_sweep_ms));
            reaper_timer.async_wait([&](std::error_code ec) {
                if (ec || io.stopped()) return;
                for (auto& sess : registry->find_stale_sessions(cfg_.reaper_timeout_ms)) {
                    std::printf("[server] reaper: dropping stale session %llu (user %u)\n",
                                static_cast<unsigned long long>(sess->session_id()),
                                sess->user_id());
                    sess->close();
                }
                arm_reaper();
            });
        };
        arm_reaper();
    }

    std::printf("[voicecat-server] %s — TCP :%u  UDP :%u\n",
                cfg_.server_name.c_str(), bound, media_bound);
    std::printf("[voicecat-server] fingerprint: %s\n",
                id_mgr.fingerprint_display().c_str());

    io.run();

    // Close all connections and block until their TLS I/O threads have finished.
    // MUST happen before io (and its reactor) is destroyed — the TLS read threads do
    // blocking I/O (not async on io_context) and touch the reactor on socket close.
    // On macOS kqueue the reactor pointer is null'd immediately on io_context destruction,
    // causing a segfault; latent on Windows IOCP / Linux epoll where timing is more forgiving.
    acceptor.shutdown();

    {
        std::lock_guard lk(stop_mutex_);
        stop_fn_ = nullptr;
    }

    workers->join();
    return 0;
}

void Server::stop() {
    std::lock_guard lk(stop_mutex_);
    if (stop_fn_) stop_fn_();
}

}  // namespace voicecat::server

#else  // !VOICECAT_HAS_NET

namespace voicecat::server {

int Server::run() {
    std::fprintf(stderr, "[server] stub: VOICECAT_HAS_NET not defined (build with dev preset)\n");
    std::printf("  server_name : %s\n", cfg_.server_name.c_str());
    std::printf("  data_dir    : %s\n", cfg_.data_dir.c_str());
    std::printf("  bind_port   : %u\n", cfg_.bind_port);
    return 0;
}

void Server::stop() {}

}  // namespace voicecat::server

#endif  // VOICECAT_HAS_NET
