// dala_beam.m — Dala BEAM launcher for iOS.
// Extracted from the per-app beam_main.m stub so app code stays minimal.
// dala_set_startup_phase/error are implemented in dala_nif.m (which imports the
// Swift-generated header) so this file stays free of app-specific includes.

#import <Foundation/Foundation.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <ifaddrs.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "dala_beam.h"

// EPMD compiled into the binary (epmd.c / epmd_srv.c / epmd_cli.c compiled
// with -Dmain=epmd_ios_main). Only present in device builds; the simulator
// connects to the Mac's EPMD via the shared network stack.
//
// DALA_RELEASE: App Store builds (mix dala.release) drop EPMD entirely so the
// shipped binary has no distribution surface — Apple is unhappy with apps
// that listen on arbitrary network ports for remote-code-execution-shaped
// traffic, and TestFlight review may flag it. The BEAM still boots, the NIF
// still works, but the app is networkless from a distribution POV.
#if defined(DALA_BUNDLE_OTP) && !defined(DALA_RELEASE)
extern int epmd_ios_main(int argc, char **argv);
static void* epmd_thread(void *arg) {
    char *args[] = {"epmd", NULL};
    epmd_ios_main(1, args);  // runs the EPMD event loop (does not return)
    return NULL;
}
#endif

// Compile-time defaults (simulator). Override via -D flags for device builds.
//
// On simulator the OTP runtime is not bundled in the .app — it's written to a
// directory on the Mac filesystem that the simulator app can read. The legacy
// path was hardcoded to /tmp/otp-ios-sim; dala_new ≥ 0.1.20 instead resolves
// the runtime dir from the DALA_SIM_RUNTIME_DIR env var (passed by mix dala.deploy
// via simctl's SIMCTL_CHILD_* mechanism) and defaults to ~/.dala/runtime/ios-sim.
//
// resolve_sim_otp_root() below handles both paths so old and new projects work
// against the same compiled dala_beam.m.
#ifndef OTP_ROOT_LEGACY
#define OTP_ROOT_LEGACY "/tmp/otp-ios-sim"
#endif
#ifndef ERTS_VSN
#define ERTS_VSN   "erts-16.3"
#endif
#ifndef OTP_RELEASE
#define OTP_RELEASE "29"
#endif

// Resolve the simulator's OTP runtime root at startup.
// Order:
//   1. DALA_SIM_RUNTIME_DIR env var if set (passed via SIMCTL_CHILD_*)
//   2. /tmp/otp-ios-sim if it exists (legacy projects, pre-dala_new 0.1.20)
//   3. /tmp/otp-ios-sim otherwise (final fallback to keep behaviour stable
//      when nothing else is found — the launching mix dala.deploy is in charge
//      of setting the env var when the project supports it)
static const char *resolve_sim_otp_root(void) {
    const char *env = getenv("DALA_SIM_RUNTIME_DIR");
    if (env && env[0]) return env;
    return OTP_ROOT_LEGACY;
}

void dala_init_ui(void) {
    // SwiftUI: the UI is driven by DalaViewModel (Swift ObservableObject).
    // No UIViewController reference needed here; the hosting controller is
    // created by DalaUIFactory in AppDelegate.m.
    NSLog(@"[DalaBeam] dala_init_ui: SwiftUI mode ready");
}

static void dala_write_diag(const char *docs_dir, const char *name, const char *info) {
    char path[1024];
    snprintf(path, sizeof(path), "%s/%s", docs_dir, name);
    FILE *f = fopen(path, "w");
    if (f) { fprintf(f, "%s\n", info); fclose(f); }
}

// Find the device's own USB link-local (169.254.x.x) IP by walking ifaddrs.
// On simulator there is no such interface; returns NULL so callers fall back to 127.0.0.1.
static const char *find_link_local_ip(char *buf, size_t len) {
    struct ifaddrs *ifa_list;
    if (getifaddrs(&ifa_list) != 0) return NULL;
    const char *found = NULL;
    for (struct ifaddrs *ifa = ifa_list; ifa && !found; ifa = ifa->ifa_next) {
        if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET) continue;
        struct sockaddr_in *sa = (struct sockaddr_in *)ifa->ifa_addr;
        uint32_t addr = ntohl(sa->sin_addr.s_addr);
        if ((addr >> 16) == 0xA9FE) {  // 169.254.0.0/16
            inet_ntop(AF_INET, &sa->sin_addr, buf, (socklen_t)len);
            found = buf;
        }
    }
    freeifaddrs(ifa_list);
    return found;
}

// Find a routable LAN IP (10.x.x.x, 172.16-31.x.x, 192.168.x.x) for WiFi distribution
// when no USB link-local interface is present. Returns NULL if none found.
static const char *find_lan_ip(char *buf, size_t len) {
    struct ifaddrs *ifa_list;
    if (getifaddrs(&ifa_list) != 0) return NULL;
    const char *found = NULL;
    for (struct ifaddrs *ifa = ifa_list; ifa && !found; ifa = ifa->ifa_next) {
        if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET) continue;
        struct sockaddr_in *sa = (struct sockaddr_in *)ifa->ifa_addr;
        uint32_t addr = ntohl(sa->sin_addr.s_addr);
        uint32_t top8  = addr >> 24;
        uint32_t top16 = addr >> 16;
        if (top8 == 10 ||                               // 10.0.0.0/8
            (top16 >= 0xAC10 && top16 <= 0xAC1F) ||   // 172.16.0.0/12
            top16 == 0xC0A8 ||                          // 192.168.0.0/16
            (top16 >= 0x6440 && top16 <= 0x647F)) {   // 100.64.0.0/10 (Tailscale)
            inet_ntop(AF_INET, &sa->sin_addr, buf, (socklen_t)len);
            found = buf;
        }
    }
    freeifaddrs(ifa_list);
    return found;
}

void dala_start_beam(const char* app_module) {
    dala_set_startup_phase("Setting up BEAM environment…");

    // Resolve Documents dir early for diagnostics.
    NSArray *dp = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *docs_ns = [dp firstObject];
    const char *docs_dir = docs_ns ? [docs_ns UTF8String] : "/tmp";
    dala_write_diag(docs_dir, "dala_diag_a_entered.txt", "dala_start_beam entered");

    // On physical device the OTP runtime is bundled inside the .app — resolve
    // the path at runtime so it works regardless of where iOS installs the app.
    // On simulator the runtime lives on the Mac filesystem; resolve_sim_otp_root()
    // reads DALA_SIM_RUNTIME_DIR (set by mix dala.deploy via simctl) with a /tmp
    // fallback for legacy projects.
#ifdef DALA_BUNDLE_OTP
    NSString *bundle_otp = [[[NSBundle mainBundle] bundlePath]
                             stringByAppendingPathComponent:@"otp"];
    const char *otp_root = [bundle_otp UTF8String];
    const char *erts_vsn = ERTS_VSN;
    const char *otp_release = OTP_RELEASE;
#else
    const char *otp_root = resolve_sim_otp_root();
    const char *erts_vsn = ERTS_VSN;
    const char *otp_release = OTP_RELEASE;
#endif

    dala_write_diag(docs_dir, "dala_diag_b_otp_root.txt", otp_root);

    // Compose dynamic paths that depend on otp_root.
    static char bindir[512], elixir_dir[512], logger_dir[512], boot_path[512];
    snprintf(bindir,      sizeof(bindir),      "%s/%s/bin",              otp_root, erts_vsn);
    snprintf(elixir_dir,  sizeof(elixir_dir),  "%s/lib/elixir/ebin",    otp_root);
    snprintf(logger_dir,  sizeof(logger_dir),  "%s/lib/logger/ebin",    otp_root);
    snprintf(boot_path,   sizeof(boot_path),   "%s/releases/%s/start_clean", otp_root, otp_release);

    dala_write_diag(docs_dir, "dala_diag_c_paths.txt", bindir);
    NSLog(@"[DalaBeam] otp_root=%s erts=%s release=%s", otp_root, erts_vsn, otp_release);

    setenv("BINDIR",   bindir, 1);
    setenv("ROOTDIR",  otp_root, 1);
    setenv("PROGNAME", "erl", 1);
    setenv("EMU",      "beam", 1);
    setenv("HOME",     "/tmp", 1);
    // Set DALA_DATA_DIR to the app's Documents directory — persistent storage
    // accessible to the app and backed up by iCloud. Used by the generated Repo
    // module to determine where to place the SQLite database file.
    // Falls back to /tmp when the documents path is unavailable (e.g. on simulator
    // when the sandbox isn't fully resolved at BEAM launch time).
    setenv("DALA_DATA_DIR", docs_dir, 1);

    // Write crash dump to app's Documents so it survives the crash and can be retrieved.
    static char crash_dump[512];
    snprintf(crash_dump, sizeof(crash_dump), "%s/dala_erl_crash.dump", docs_dir);
    setenv("ERL_CRASH_DUMP", crash_dump, 1);
    setenv("ERL_CRASH_DUMP_SECONDS", "30", 1);

    // Dist port: read from DALA_DIST_PORT env var (simulator via SIMCTL_CHILD_ prefix),
    // default to 9101 for standalone/physical launch.
    const char *env_port = getenv("DALA_DIST_PORT");
    static char dist_port_min[16], dist_port_max[16];
    snprintf(dist_port_min, sizeof(dist_port_min), "%s", env_port ? env_port : "9101");
    snprintf(dist_port_max, sizeof(dist_port_max), "%s", env_port ? env_port : "9101");

    // Determine node hostname:
    //   DALA_BUNDLE_OTP = physical device build (OTP bundled in .app).
    //   Priority: WiFi/LAN (10/172/192.168/Tailscale) > USB link-local (169.254.x.x) > 127.0.0.1
    //
    //   WiFi is preferred over USB because the node name is fixed at startup.
    //   If USB were preferred, unplugging the cable would strand the node at a
    //   link-local address that is no longer reachable — requiring an app restart
    //   to regain connectivity. With WiFi first, the node stays reachable on the
    //   same IP whether the cable is plugged in or not.
    //   USB link-local is the fallback for cable-only setups (no WiFi).
    //   127.0.0.1 is last resort; dist only reachable via iproxy in that case.
    //   The in-process EPMD and dist port both bind 0.0.0.0, so the node is
    //   reachable via any interface regardless of which IP was chosen as the name.
    //
    //   Without DALA_BUNDLE_OTP = simulator build. Simulator shares the Mac's network
    //   stack, including Mac's USB link-local interfaces, so find_link_local_ip()
    //   would return the Mac's USB IP (wrong). Always use 127.0.0.1 on simulator.
#ifdef DALA_BUNDLE_OTP
    // Physical device: WiFi/LAN → USB link-local → loopback fallback.
    static char lan_ip_buf[64], link_local_buf[64];
    const char *lan_ip = find_lan_ip(lan_ip_buf, sizeof(lan_ip_buf));
    const char *ll_ip  = lan_ip ? NULL : find_link_local_ip(link_local_buf, sizeof(link_local_buf));
    const char *host_ip = lan_ip ? lan_ip : (ll_ip ? ll_ip : "127.0.0.1");
    static char eval_expr[280], node_name[128], beams_dir[512];
    snprintf(eval_expr, sizeof(eval_expr), "%s:start().", app_module);
    snprintf(node_name, sizeof(node_name), "%s_ios@%s", app_module, host_ip);
#else
    // Simulator: use 127.0.0.1 but include a short UDID suffix so concurrent
    // simulators get unique node names and don't conflict in Mac's EPMD.
    // SIMULATOR_UDID is set automatically by the iOS simulator runtime.
    const char *host_ip = "127.0.0.1";
    const char *sim_udid = getenv("SIMULATOR_UDID");
    static char sim_short[9];
    sim_short[0] = '\0';
    if (sim_udid) {
        int n = 0;
        for (int i = 0; sim_udid[i] && n < 8; i++) {
            unsigned char c = (unsigned char)sim_udid[i];
            if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) {
                sim_short[n++] = (char)tolower(c);
            }
        }
        sim_short[n] = '\0';
    }
    static char eval_expr[280], node_name[128], beams_dir[512];
    snprintf(eval_expr, sizeof(eval_expr), "%s:start().", app_module);
    if (sim_short[0]) {
        snprintf(node_name, sizeof(node_name), "%s_ios_%s@%s", app_module, sim_short, host_ip);
    } else {
        snprintf(node_name, sizeof(node_name), "%s_ios@%s", app_module, host_ip);
    }
#endif
    dala_write_diag(docs_dir, "dala_diag_host_ip.txt", host_ip);
    snprintf(beams_dir, sizeof(beams_dir), "%s/%s", otp_root, app_module);

#ifdef DALA_BUNDLE_OTP
    // On physical device, the bundle BEAMs are read-only (code-signed).
    // deployer.ex can push updated BEAMs to Documents/otp/<app>/ via
    // `xcrun devicectl device copy to --domain-type appDataContainer`.
    // If that directory exists, prefer it over the in-bundle copy.
    static char docs_beams[512];
    snprintf(docs_beams, sizeof(docs_beams), "%s/otp/%s", docs_dir, app_module);
    if ([[NSFileManager defaultManager] fileExistsAtPath:@(docs_beams)])  {
        strlcpy(beams_dir, docs_beams, sizeof(beams_dir));
    }
    dala_write_diag(docs_dir, "dala_diag_beams_dir.txt", beams_dir);
#endif

    // DALA_BEAMS_DIR — the directory where app BEAMs (and priv/) are deployed.
    //
    // Ecto.Migrator uses :code.priv_dir(app) to locate migration .exs files, but
    // that requires an OTP lib structure ($OTP_ROOT/lib/APP-VERSION/ebin/). Dala
    // apps use a flat -pa directory, so :code.priv_dir/1 returns {error, bad_name}
    // and Ecto silently reports "Migrations already up" without running anything.
    //
    // Fix: deployer.ex copies priv/ into beams_dir/priv/. App code reads this
    // env var and passes the explicit path to Ecto.Migrator.run/4. See also the
    // corresponding comment in dala_beam.c for the Android side.
    setenv("DALA_BEAMS_DIR", beams_dir, 1);

    // Compile-time default BEAM tuning flags.
    // Overridden at runtime if beams_dir/dala_beam_flags exists
    // (written by `mix dala.deploy --schedulers N` or `--beam-flags "..."`).
    static const char* s_default_flags[] = {
        "-S", "1:1", "-SDcpu", "1:1", "-SDio", "1", "-A", "1", "-sbwt", "none",
        NULL
    };

    // Runtime override: read whitespace-separated flags from beams_dir/dala_beam_flags.
    static char   s_flags_buf[512]         = {0};
    static const char* s_runtime_flags[64] = {NULL};
    static int    s_runtime_flag_count     = 0;
    {
        char flags_path[640];
        snprintf(flags_path, sizeof(flags_path), "%s/dala_beam_flags", beams_dir);
        FILE *f = fopen(flags_path, "r");
        if (f) {
            size_t n = fread(s_flags_buf, 1, sizeof(s_flags_buf) - 1, f);
            fclose(f);
            s_flags_buf[n] = '\0';
            s_runtime_flag_count = 0;
            char *p = s_flags_buf;
            while (*p && s_runtime_flag_count < 63) {
                while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
                if (!*p) break;
                s_runtime_flags[s_runtime_flag_count++] = p;
                while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++;
                if (*p) *p++ = '\0';
            }
            s_runtime_flags[s_runtime_flag_count] = NULL;
            NSLog(@"[DalaBeam] loaded %d runtime flags from %s", s_runtime_flag_count, flags_path);
        }
    }

    const char** selected_flags = (s_runtime_flag_count > 0)
        ? s_runtime_flags
        : s_default_flags;

    static const char* args[128];
    int ac = 0;
    args[ac++] = "beam";
    for (int i = 0; selected_flags[i]; i++) args[ac++] = selected_flags[i];
    // Cap the BEAM's memory super carrier to 10MB on physical iOS devices.
    // The default 1GB virtual reservation is rejected by iOS on real hardware
    // (not on simulator where the Mac's VM handles it). Without this the BEAM
    // crashes immediately during startup on any physical iOS device.
#ifdef DALA_BUNDLE_OTP
    args[ac++] = "-MIscs"; args[ac++] = "10";
#endif
    args[ac++] = "--";
    args[ac++] = "-root";       args[ac++] = otp_root;
    args[ac++] = "-bindir";     args[ac++] = bindir;
    args[ac++] = "-progname";   args[ac++] = "erl";
    args[ac++] = "--";
#ifndef DALA_RELEASE
    // Distribution flags. Omitted for App Store builds — see DALA_RELEASE
    // notes at the top of this file.
    args[ac++] = "-name";       args[ac++] = node_name;
    args[ac++] = "-setcookie";  args[ac++] = "dala_secret";
    args[ac++] = "-kernel"; args[ac++] = "inet_dist_listen_min"; args[ac++] = dist_port_min;
    args[ac++] = "-kernel"; args[ac++] = "inet_dist_listen_max"; args[ac++] = dist_port_max;
#else
    // Mark DALA_RELEASE in env so Dala.Dist.ensure_started/1 short-circuits
    // before trying Node.start (which would fail without -name anyway, but
    // the env var lets app code probe for release mode without parsing
    // erl args).
    setenv("DALA_RELEASE", "1", 1);
    (void)dist_port_min; (void)dist_port_max; (void)node_name;
#endif
    args[ac++] = "-noshell";
    args[ac++] = "-noinput";
    args[ac++] = "-boot";   args[ac++] = boot_path;
    args[ac++] = "-pa";     args[ac++] = elixir_dir;
    args[ac++] = "-pa";     args[ac++] = logger_dir;
    args[ac++] = "-pa";     args[ac++] = beams_dir;
    args[ac++] = "-eval";   args[ac++] = eval_expr;
    args[ac] = NULL;
    NSLog(@"[DalaBeam] dala_start_beam: starting BEAM module=%s argc=%d", app_module, ac);
    dala_set_startup_phase("Starting BEAM…");
    dala_write_diag(docs_dir, "dala_diag_d_erl_start.txt", "calling erl_start");

    // Redirect stdout/stderr to a log file so BEAM error output is captured.
    char beam_log_path[512];
    snprintf(beam_log_path, sizeof(beam_log_path), "%s/beam_stdout.log", docs_dir);
    int log_fd = open(beam_log_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (log_fd >= 0) {
        dup2(log_fd, STDOUT_FILENO);
        dup2(log_fd, STDERR_FILENO);
        close(log_fd);
    }

#if defined(DALA_BUNDLE_OTP) && !defined(DALA_RELEASE)
    // Start EPMD as a thread so the BEAM can register for distribution.
    // On simulator the Mac's EPMD is reachable via the shared network stack;
    // on device there is no host EPMD, so we run our own in-process.
    // App Store builds (DALA_RELEASE) drop EPMD entirely.
    pthread_t epmd_t;
    pthread_create(&epmd_t, NULL, epmd_thread, NULL);
    pthread_detach(epmd_t);
    usleep(300000);  // 300ms — give EPMD time to bind port 4369
#endif

    void erl_start(int, char**);
    erl_start(ac, (char**)args);
    dala_write_diag(docs_dir, "dala_diag_e_erl_exited.txt", "erl_start returned");
    dala_set_startup_error("BEAM exited unexpectedly — check Documents/dala_erl_crash.dump");
    NSLog(@"[DalaBeam] dala_start_beam: erl_start returned (unexpected)");
}
