#!/bin/sh
#
# S82dcentrald - DCENT_OS mining daemon (platform-aware: Zynq + serial SoCs)
#
# dcentrald owns REST/WebSocket on :8080. The dashboard owns :80.
#

DAEMON="/usr/local/bin/dcentrald"
CONFIG="/etc/dcentrald/dcentrald.toml"
[ -f /data/dcentrald.toml ] && CONFIG="/data/dcentrald.toml"
[ ! -f "$CONFIG" ] && [ -f /etc/dcentrald.toml ] && CONFIG="/etc/dcentrald.toml"
LOGFILE="/tmp/dcentrald.log"
PIDFILE="/var/run/dcentrald.pid"
CHILD_PIDFILE="/var/run/dcentrald-child.pid"
EXPECTFILE="/var/run/dcentrald.expected_exit.pid"
API_PORT=8080
SCRIPT="$0"
SESSION_LATCH_HELPER="/usr/libexec/dcentos/dcentrald-session-latch.sh"
BOARD_SETUP="/etc/init.d/S37board_setup"
OPERATION_LOCK="/run/dcentos/dcentrald-lifecycle-operation.lock"
MUTATION_POLICY_FILE="/etc/dcentos/mutation_policy"
MUTATION_POLICY_HELPER="/usr/libexec/dcentos/mutation-policy.sh"

require_runtime_authority() {
    if [ ! -r "$MUTATION_POLICY_HELPER" ]; then
        echo "  [FAIL] mutation-policy helper is missing; refusing hardware lifecycle action"
        return 1
    fi
    . "$MUTATION_POLICY_HELPER"
    if ! dcent_mutation_policy_has "$MUTATION_POLICY_FILE" daemon-hardware; then
        echo "  [FAIL] daemon-hardware capability is missing or insecure; refusing hardware lifecycle action"
        return 1
    fi
}

acquire_operation_lock() {
    mkdir -p /run/dcentos 2>/dev/null || return 1
    chmod 0700 /run/dcentos 2>/dev/null || return 1
    mkdir "$OPERATION_LOCK" 2>/dev/null || {
        echo "  [FAIL] Another dcentrald lifecycle operation is active or stale"
        return 1
    }
    printf '%s\n' "$$" > "$OPERATION_LOCK/pid" 2>/dev/null || {
        rmdir "$OPERATION_LOCK" 2>/dev/null || true
        return 1
    }
    trap 'release_operation_lock' 0
    trap 'exit 129' HUP
    trap 'exit 130' INT
    trap 'exit 143' TERM
}

release_operation_lock() {
    rm -f "$OPERATION_LOCK/pid" 2>/dev/null || true
    rmdir "$OPERATION_LOCK" 2>/dev/null || true
}

detect_platform() {
    IMAGE_PLATFORM=$(cat /etc/dcentos/platform 2>/dev/null || true)
    IMAGE_TARGET=$(cat /etc/dcentos/board_target 2>/dev/null || true)
    IMAGE_RAIL=$(cat /etc/dcentos/rail_gpio 2>/dev/null || true)
    case "$IMAGE_PLATFORM:$IMAGE_TARGET" in
        am3-aml-s19jpro:am3-s19jpro-aml|\
        am3-aml-s19k:am3-s19k|\
        am3-aml-s19kpro:am3-s19kpro|\
        am3-aml-s21:am3-s21|\
        am3-aml-s21pro:am3-s21pro|\
        am3-aml-s21xp:am3-s21xp|\
        am3-aml-s19xp:am3-s19xp|\
        am3-aml-s19jxp:am3-s19jxp|\
        am3-aml-s19jproa:am3-s19jproa|\
        am3-aml-s19jproplus:am3-s19jproplus)
            echo "amlogic"
            return
            ;;
    esac
    # A partial image may lose one identity marker. GPIO437 is a sealed fixed
    # safety gate for this image family and remains sufficient to select the
    # de-energize-only path, never to authorize mining.
    [ "$IMAGE_RAIL" = 437 ] && { echo "amlogic"; return; }

    if [ -e /dev/uio0 ]; then
        echo "zynq"
    elif grep -qiE 'AM33XX|AM335x|TI AM335' /proc/cpuinfo 2>/dev/null || [ -e /dev/ttyO1 ]; then
        echo "beaglebone"
    elif [ -e /dev/ttyS1 ] || [ -e /dev/ttyS2 ]; then
        echo "amlogic"
    else
        echo "unknown"
    fi
}

child_identity_matches() {
    PID=$1
    START_TICKS=$2
    [ -n "$PID" ] && [ -n "$START_TICKS" ] || return 1
    case "$PID:$START_TICKS" in
        *[!0-9:]*|:*) return 1 ;;
    esac
    [ "$(awk '{print $22}' "/proc/$PID/stat" 2>/dev/null)" = "$START_TICKS" ] || return 1
    [ "$(readlink -f "/proc/$PID/exe" 2>/dev/null)" = "$DAEMON" ] || return 1
}

child_identity_is_live() {
    child_identity_matches "$1" "$2" || return 1
    STATE=$(awk '{print $3}' "/proc/$1/stat" 2>/dev/null) || return 1
    [ "$STATE" != Z ]
}

dcentrald_may_execute() {
    for PID in $(pidof dcentrald 2>/dev/null || true); do
        case "$PID" in
            ''|*[!0-9]*) return 0 ;;
        esac
        STATE=$(awk '{print $3}' "/proc/$PID/stat" 2>/dev/null) || return 0
        [ "$STATE" = Z ] || return 0
    done
    return 1
}

migrate_legacy_api_port() {
    [ -f "$CONFIG" ] || return 0

    # Rewrite legacy dcentrald API http_port 80 to 8080 so the dashboard owns :80.
    if ! grep -Eq '^[[:space:]]*http_port[[:space:]]*=[[:space:]]*80([[:space:]]*(#.*)?)?$' "$CONFIG" 2>/dev/null; then
        return 0
    fi

    TMPFILE="${CONFIG}.tmp.$$"
    if awk '
        BEGIN { in_api = 0 }
        /^[[:space:]]*\[[^]]+\][[:space:]]*$/ {
            in_api = ($0 ~ /^[[:space:]]*\[api\][[:space:]]*$/)
        }
        in_api && /^[[:space:]]*http_port[[:space:]]*=[[:space:]]*80([[:space:]]*(#.*)?)?$/ {
            sub(/80/, "8080")
        }
        { print }
    ' "$CONFIG" > "$TMPFILE" 2>/dev/null; then
        if mv "$TMPFILE" "$CONFIG" 2>/dev/null; then
            echo "  [MIGRATE] dcentrald API http_port 80 to 8080 in $CONFIG"
        else
            rm -f "$TMPFILE"
            echo "  [WARN] Could not migrate legacy dcentrald API port in $CONFIG"
        fi
    else
        rm -f "$TMPFILE"
        echo "  [WARN] Could not inspect dcentrald API port in $CONFIG"
    fi
}

fan_safety_override() {
    FAN_PLATFORM=${PLATFORM:-$(detect_platform)}
    fan_detail="fans commanded to safe PWM (dcentrald exited)"
    fan_result=0
    case "$FAN_PLATFORM" in
        amlogic)
            if [ -d /sys/class/pwm/pwmchip0/pwm0 ]; then
                # Amlogic A113D sysfs PWM: period=100000ns (10kHz). PWM 30 of 100
                # is duty=30000ns. Matches Rust runtime helper
                # amlogic_pwm_percent_to_duty_ns(pwm=30, period=100000) = 30000.
                # NEVER above PWM 30 â€” see feedback_fan_max_30pwm /
                # feedback_fan_never_blast.
                for channel in 0 1; do
                    duty=/sys/class/pwm/pwmchip0/pwm${channel}/duty_cycle
                    period=/sys/class/pwm/pwmchip0/pwm${channel}/period
                    enabled=/sys/class/pwm/pwmchip0/pwm${channel}/enable
                    if [ ! -e "$duty" ] || [ ! -e "$period" ] || [ ! -e "$enabled" ] \
                        || [ "$(cat "$period" 2>/dev/null)" != 100000 ] \
                        || [ "$(cat "$enabled" 2>/dev/null)" != 1 ] \
                        || ! printf '30000\n' > "$duty" 2>/dev/null \
                        || [ "$(cat "$duty" 2>/dev/null)" != 30000 ]; then
                        fan_result=1
                        fan_detail="WARN: Amlogic PWM30 period/duty/enable evidence failed on channel $channel"
                    fi
                done
            else
                fan_result=1
                fan_detail="WARN: Amlogic PWM controller is unavailable"
            fi
            ;;
        beaglebone)
            # AM335x BB carrier (S19j Pro 3-board variant) â€” 4 fans on sysfs PWM.
            # Stock S70cgminer drove all groups in lockstep; we mirror the same
            # PWM=30 (30000ns / 100000ns period) across every exported channel.
            # Both LuxOS/DCENT_OS pwmchip0+pwmchip2 paths and Bitmain stock
            # legacy pwm1/pwm2 are tolerated.
            for p in /sys/class/pwm/pwmchip0/pwm0/duty_cycle \
                     /sys/class/pwm/pwmchip0/pwm1/duty_cycle \
                     /sys/class/pwm/pwmchip2/pwm0/duty_cycle; do
                [ -e "$p" ] && echo 30000 > "$p" 2>/dev/null
            done
            for p in /sys/class/pwm/pwm1/duty_ns /sys/class/pwm/pwm2/duty_ns; do
                [ -e "$p" ] && echo 30000 > "$p" 2>/dev/null
            done
            ;;
        zynq)
            # AM2/XIL fan-control is UIO-bound; devmem writes are stale/no-op.
            # NOTE: this is the amlogic+beaglebone overlay; detect_platform here
            # returns "amlogic"/"beaglebone", so this zynq arm is a defensive
            # branch that does NOT execute on these boards. The canonical AM2
            # fan-hold fix (persistent `dcentrald --hold-fan` custodian, since a
            # bare --set-fan one-shot lets the AM2 board revert fans to full
            # speed â€” feedback_am2_never_leave_unit_without_fan_manager.md) lives
            # in the board/zynq/* overlays that actually ship to AM2 units. This
            # branch is intentionally left as the simple one-shot because it is
            # unreachable on this overlay; do not duplicate the custodian helper
            # here.
            if [ -x "$DAEMON" ]; then
                if "$DAEMON" --set-fan 30 >/tmp/s82dcentrald-fan-safety.log 2>&1; then
                    fan_detail="AM2 fans commanded to PWM 30 via dcentrald UIO (one-shot; AM2 hold lives in board/zynq/*)"
                else
                    fan_detail="WARN: AM2 fan safety command failed; see /tmp/s82dcentrald-fan-safety.log"
                fi
            else
                fan_detail="WARN: AM2 fan safety command skipped; $DAEMON missing"
            fi
            ;;
    esac
    echo "$(date): SAFETY: $fan_detail" >> "$LOGFILE"
    return "$fan_result"
}

verify_amlogic_boot_safe_state() {
    [ -x "$BOARD_SETUP" ] || {
        echo "$(date): WARN: checked Amlogic board owner is missing" >> "$LOGFILE"
        return 1
    }
    "$BOARD_SETUP" verify-start-safe-state >> "$LOGFILE" 2>&1
}

emergency_hardware_safe_state() {
    SAFETY_PLATFORM=${PLATFORM:-$(detect_platform)}
    case "$SAFETY_PLATFORM" in
        amlogic)
            [ -x "$BOARD_SETUP" ] || return 1
            "$BOARD_SETUP" emergency-safe-off >> "$LOGFILE" 2>&1
            ;;
        *)
            [ -x "$DAEMON" ] || return 1
            "$DAEMON" --safe-off >> "$LOGFILE" 2>&1 || return 1
            fan_safety_override
            ;;
    esac
}

restart_after_software_closeout() {
    "$SCRIPT" stop || return $?
    # The supervisor consumes the exact daemon proof after wait(2), then
    # removes only its unresolved marker. Stop returning zero is insufficient.
    RESTART_ATTEMPTS=10
    while [ "$RESTART_ATTEMPTS" -gt 0 ]; do
        if RESTART_STATE=$(/bin/sh "$SESSION_LATCH_HELPER" status); then
            RESTART_STATUS=0
        else
            RESTART_STATUS=$?
        fi
        case "$RESTART_STATUS:$RESTART_STATE" in
            0:clear) "$SCRIPT" start; return $? ;;
            1:unresolved|3:admission-locked) ;;
            *) echo "  [FAIL] Restart refused: supervisor disposition is $RESTART_STATE"; return 1 ;;
        esac
        RESTART_ATTEMPTS=$((RESTART_ATTEMPTS - 1))
        [ "$RESTART_ATTEMPTS" -eq 0 ] || sleep 1
    done
    echo "  [FAIL] Restart refused: exact orderly software closeout was not consumed"
    return 1
}

case "$1" in
    start|stop|restart|safety)
        require_runtime_authority || exit 1
        ;;
esac

case "$1" in
    start)
        # Only start requires storage readiness. Stop/safety must remain
        # available if persistence disappears while the hardware owner runs.
        if [ "$(cat /etc/dcentos/board_target 2>/dev/null)" = am3-s19jpro-aml ]; then
            /usr/bin/python3 -I /usr/libexec/dcentos/classic-aml-storage.py check || {
                echo "  [FAIL] Classic AML persistent namespace is not ready"
                exit 1
            }
        fi
        case "$(cat /etc/dcentos/board_target 2>/dev/null)" in
            am3-s19jproa|am3-s19jproplus)
                /usr/bin/python3 -I /usr/libexec/dcentos/bm1362-nopic-storage.py check || {
                    echo "  [FAIL] A126/Plus120 persistent namespace is not ready"
                    exit 1
                }
                ;;
            am3-s19kpro)
                /usr/bin/python3 -I /usr/libexec/dcentos/s19kpro-nopic-storage.py check || {
                    echo "  [FAIL] S19k Pro persistent namespace is not ready"
                    exit 1
                }
                ;;
        esac
        acquire_operation_lock || exit 1
        PLATFORM=$(detect_platform)
        [ "$PLATFORM" != unknown ] || {
            echo "  [FAIL] Platform identity and device discovery are both unknown"
            exit 1
        }
        if [ "$PLATFORM" = amlogic ] && ! verify_amlogic_boot_safe_state; then
            echo "  [FAIL] Amlogic boot-safe receipt/revalidation is unavailable"
            emergency_hardware_safe_state || true
            exit 1
        fi
        [ -x "$DAEMON" ] || {
            echo "  [FAIL] $DAEMON not found"
            emergency_hardware_safe_state || true
            exit 1
        }
        [ -f "$CONFIG" ] || {
            echo "  [FAIL] $CONFIG not found"
            emergency_hardware_safe_state || true
            exit 1
        }

        # W1.5 (2026-05-07): re-assert tight perms on /data/dcent/ + auth.json
        # before launching the daemon. dcentrald will also auto-correct via
        # verify_auth_file_perms() at startup, but doing it here closes the
        # window between mount(/data) and the first daemon write.
        if [ -d /data/dcent ]; then
            chmod 0700 /data/dcent 2>/dev/null
        fi
        if [ -f /data/dcent/auth.json ]; then
            chmod 0600 /data/dcent/auth.json 2>/dev/null
        fi

        migrate_legacy_api_port

        RUNNING_PIDS=$(pidof dcentrald 2>/dev/null || true)
        if [ -n "$RUNNING_PIDS" ]; then
            echo "  [FAIL] Refusing a second dcentrald owner (PID(s): $RUNNING_PIDS)"
            exit 1
        fi
        if [ ! -r "$SESSION_LATCH_HELPER" ]; then
            echo "  [FAIL] Persistent hardware-session latch helper is missing"
            emergency_hardware_safe_state || true
            exit 1
        fi
        SESSION_TOKEN=$(/bin/sh "$SESSION_LATCH_HELPER" prepare 2>> "$LOGFILE") || {
            echo "  [FAIL] Hardware-session admission is blocked; see $LOGFILE"
            emergency_hardware_safe_state || true
            exit 1
        }
        if ! fan_safety_override; then
            /bin/sh "$SESSION_LATCH_HELPER" abandon "$SESSION_TOKEN" prelaunch-cooling-failed \
                >> "$LOGFILE" 2>&1 || true
            emergency_hardware_safe_state || true
            echo "  [FAIL] Prelaunch cooling command/readback failed"
            exit 1
        fi
        if [ "$PLATFORM" = amlogic ] \
            && ! "$BOARD_SETUP" mark-runtime-handoff >> "$LOGFILE" 2>&1; then
            /bin/sh "$SESSION_LATCH_HELPER" abandon "$SESSION_TOKEN" boot-handoff-failed \
                >> "$LOGFILE" 2>&1 || true
            emergency_hardware_safe_state || true
            echo "  [FAIL] Amlogic boot-to-runtime handoff failed"
            exit 1
        fi

        echo "  Starting dcentrald (platform=$PLATFORM)..."
        echo "  Config: $CONFIG"
        echo "  API: REST/WebSocket :$API_PORT"
        echo "  Dashboard: HTTP :80 via S80dashboard"
        case "$PLATFORM" in
            amlogic|beaglebone) set -- "$DAEMON" --serial-mining --config "$CONFIG" ;;
            *) set -- "$DAEMON" --config "$CONFIG" ;;
        esac
        if ! start-stop-daemon -S -b -m -p "$PIDFILE" \
            -x /bin/sh -- "$SESSION_LATCH_HELPER" supervise "$SESSION_TOKEN" \
            "$SCRIPT" "$LOGFILE" "$CHILD_PIDFILE" "$EXPECTFILE" "$@"; then
            /bin/sh "$SESSION_LATCH_HELPER" abandon "$SESSION_TOKEN" supervisor-launch-failed \
                >> "$LOGFILE" 2>&1 || true
            emergency_hardware_safe_state || true
            echo "  [FAIL] dcentrald supervisor did not start"
            exit 1
        fi

        sleep 3
        if pidof dcentrald > /dev/null 2>&1; then
            echo "  [OK] dcentrald running (PID $(pidof dcentrald), REST/WebSocket :$API_PORT)"
        else
            echo "  [FAIL] dcentrald not started"
            emergency_hardware_safe_state || true
            exit 1
        fi
        ;;

    stop)
        acquire_operation_lock || exit 1
        PLATFORM=$(detect_platform)
        echo "  Stopping dcentrald..."
        CHILD_RECORD=$(cat "$CHILD_PIDFILE" 2>/dev/null || true)
        CHILD_PID=$(printf '%s\n' "$CHILD_RECORD" | awk '{print $1}')
        CHILD_START_TICKS=$(printf '%s\n' "$CHILD_RECORD" | awk '{print $2}')
        HAD_VERIFIED_CHILD=0
        if [ -n "$CHILD_PID" ] \
            && ! child_identity_matches "$CHILD_PID" "$CHILD_START_TICKS"; then
            if dcentrald_may_execute; then
                echo "  [FAIL] Refusing a live dcentrald with stale or mismatched child identity"
                exit 1
            fi
            echo "  [WARN] Ignoring stale child identity; no process will be signaled"
            CHILD_PID=
            CHILD_START_TICKS=
        fi
        [ -n "$CHILD_PID" ] && HAD_VERIFIED_CHILD=1
        if [ -z "$CHILD_PID" ] && dcentrald_may_execute; then
            echo "  [FAIL] A live dcentrald exists without a verified supervisor identity"
            exit 1
        fi
        [ -n "$CHILD_PID" ] && echo "$CHILD_PID" > "$EXPECTFILE"
        [ -n "$CHILD_PID" ] \
            && child_identity_matches "$CHILD_PID" "$CHILD_START_TICKS" \
            && kill -TERM "$CHILD_PID" 2>/dev/null

        for i in $(seq 1 30); do
            if [ -z "$CHILD_PID" ] \
                || ! child_identity_is_live "$CHILD_PID" "$CHILD_START_TICKS"; then
                break
            fi
            sleep 1
        done

        TIMED_OUT=0
        if [ -n "$CHILD_PID" ] \
            && child_identity_is_live "$CHILD_PID" "$CHILD_START_TICKS"; then
            echo "  [WARN] dcentrald did not exit in 30s - force killing"
            rm -f "$EXPECTFILE"
            TIMED_OUT=1
            child_identity_matches "$CHILD_PID" "$CHILD_START_TICKS" \
                && kill -9 "$CHILD_PID" 2>/dev/null
        fi

        # SIGKILL delivery is not process-death evidence. Observe the exact
        # PID as gone before permitting the monotonic emergency-cut custodian.
        for i in 1 2 3 4 5; do
            if [ -z "$CHILD_PID" ] \
                || ! child_identity_is_live "$CHILD_PID" "$CHILD_START_TICKS"; then
                break
            fi
            sleep 1
        done
        if [ -n "$CHILD_PID" ] \
            && child_identity_is_live "$CHILD_PID" "$CHILD_START_TICKS"; then
            echo "  [FAIL] dcentrald PID $CHILD_PID remains live after SIGKILL"
            exit 1
        fi

        # Give the verified child's parent one bounded scheduling interval to
        # reap it and publish its serialized terminal receipt. A zombie cannot
        # execute hardware operations, so the fallback custodian may proceed
        # after that interval even if pidof still reports the unreaped name.
        TERMINAL_SAFE=0
        if [ "$PLATFORM" = amlogic ] && [ "$HAD_VERIFIED_CHILD" = 1 ]; then
            for ATTEMPT in 1 2; do
                if "$BOARD_SETUP" verify-terminal-safe-state >> "$LOGFILE" 2>&1; then
                    TERMINAL_SAFE=1
                    break
                fi
                [ "$ATTEMPT" = 2 ] || sleep 1
            done
        fi
        # The raw start-stop-daemon wrapper PID is never signaled because it
        # lacks a start-time identity record.
        if [ "$TERMINAL_SAFE" != 1 ] && ! emergency_hardware_safe_state; then
            echo "  [FAIL] dcentrald exited but emergency hardware SafeOff failed"
            exit 1
        fi

        # The unresolved marker was durable before launch. Promote the reason
        # only after the owner is dead and the emergency cut has completed so
        # slow or failed persistence cannot delay containment.
        if [ "$TIMED_OUT" = 1 ]; then
            /bin/sh "$SESSION_LATCH_HELPER" latch forced-stop-timeout >> "$LOGFILE" 2>&1 || true
        fi

        rm -f "$PIDFILE" "$CHILD_PIDFILE"
        echo "  [OK] dcentrald stopped; software SafeOff readback recorded, physical rail remains unmeasured"
        ;;

    restart)
        restart_after_software_closeout
        exit $?
        ;;

    safety)
        # The common supervisor invokes this only after wait(2) observed the
        # runtime owner exit. Never create a competing one-shot owner.
        if dcentrald_may_execute; then
            echo "$(date): WARN: refusing a competing one-shot safety owner while dcentrald is live" >> "$LOGFILE"
            exit 1
        fi
        if emergency_hardware_safe_state; then
            echo "$(date): SAFETY: command/readback SafeOff recorded; physical rail was not measured" >> "$LOGFILE"
        else
            echo "$(date): FAIL: emergency hardware SafeOff failed; power disposition remains unknown" >> "$LOGFILE"
            fan_safety_override || true
            exit 1
        fi
        ;;

    status)
        if pidof dcentrald > /dev/null 2>&1; then
            echo "dcentrald: running (PID $(pidof dcentrald), REST/WebSocket :$API_PORT)"
        else
            echo "dcentrald: stopped"
        fi
        ;;

    *)
        echo "Usage: $0 {start|stop|restart|status|safety}"
        exit 1
        ;;
esac

exit 0
