/**
 * Copyright © 2012-2025 by Data Virtuality GmbH
 */
package com.datavirtuality.dv.core.interfaces.concurrent;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.datavirtuality.dv.core.interfaces.InterfacesPlugin;
import org.jboss.logging.Logger;

/**
 * ObservableLock — Reentrant, fair lock wrapper with:
 * - structured logging on acquire/release,
 * - tracking of owning thread and hold duration,
 * - warn threshold on long-held outermost acquisition,
 * - try-with-resources Guard,
 * - full Lock interface implementation.
 *
 * @author Alexander Fischer
 * @date 2025.08.21
 */
public final class ObservableLock implements Lock {

    // ------------------------------------------------------------------------------------
    // Constants & logger
    // ------------------------------------------------------------------------------------

    private static final Logger logger = Logger.getLogger(ObservableLock.class);

    private static final StackWalker CALLER_WALKER =
            StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);

    /**
     * Prefix for auto-generated lock names based on the creating class.
     */
    public static final String OBSERVABLE_LOCK = "ObservableLock_";

    // ------------------------------------------------------------------------------------
    // State
    // ------------------------------------------------------------------------------------

    /**
     * Fair lock to preserve acquisition order.
     */
    private final ReentrantLock lock = new ReentrantLock(true);

    /**
     * Human-readable lock name (either provided or inferred from the creator class).
     */
    private final String name;

    /**
     * Threshold in milliseconds to warn about long-held locks.
     */
    private final long warnThresholdMs;

    /**
     * The thread that first acquired (and still owns) the lock at the outermost level.
     */
    private volatile Thread owner;

    /**
     * Timestamp (ms) when the lock was first acquired by the current owner.
     */
    private volatile long firstAcquiredAtMs;

    // ------------------------------------------------------------------------------------
    // Construction
    // ------------------------------------------------------------------------------------

    /**
     * Uses an inferred name and the default warning threshold (30s).
     */
    public ObservableLock() {
        this.name = OBSERVABLE_LOCK + inferCreatorClass();
        this.warnThresholdMs = 30_000L;
    }

    /**
     * Uses an inferred name and a custom warning threshold.
     */
    public ObservableLock(long warnThresholdMs) {
        this.name = OBSERVABLE_LOCK + inferCreatorClass();
        this.warnThresholdMs = warnThresholdMs;
    }

    /**
     * Uses an explicit name and the default warning threshold (30s).
     */
    public ObservableLock(String name) {
        this.name = name;
        this.warnThresholdMs = 30_000L;
    }

    /**
     * Uses an explicit name and a custom warning threshold.
     */
    public ObservableLock(String name, long warnThresholdMs) {
        this.name = name;
        this.warnThresholdMs = warnThresholdMs;
    }

    // ------------------------------------------------------------------------------------
    // Public API — Guard (try-with-resources)
    // ------------------------------------------------------------------------------------

    /**
     * Acquire and return a Guard to be used in try-with-resources.
     */
    public Guard lockGuard() {
        return lockGuard(inferPurpose());
    }

    /**
     * Acquire and return a Guard to be used in try-with-resources.
     */
    public Guard lockGuard(String purpose) {
        lock(purpose);
        Thread t = Thread.currentThread();
        return new Guard(purpose, System.currentTimeMillis(), t.getName(), t.getId());
    }

    /**
     * Try to acquire lock within given timeout and return Guard or throw if failed.
     */
    public Guard tryLockGuard(long timeoutMs) throws InterruptedException {
        return tryLockGuard(timeoutMs, inferPurpose());
    }

    /**
     * Try to acquire lock within given timeout and return Guard or throw if failed.
     */
    public Guard tryLockGuard(long timeoutMs, String purpose) throws InterruptedException {
        boolean acquired = lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS);
        if (!acquired) {
            logTryTimeout(timeoutMs);
            throw new IllegalStateException("Lock " + name + " not acquired within " + timeoutMs + " ms");
        }
        afterAcquire(purpose);
        Thread t = Thread.currentThread();
        return new Guard(purpose, System.currentTimeMillis(), t.getName(), t.getId());
    }

    // Acquire lock with explicit purpose (helper)
    public void lock(String purpose) {
        lock.lock();
        afterAcquire(purpose);
    }

    // Called after successful acquisition: set owner and log if outermost
    private void afterAcquire(String purpose) {
        boolean firstAcquisition = (lock.getHoldCount() == 1);
        if (firstAcquisition) {
            owner = Thread.currentThread();
            firstAcquiredAtMs = System.currentTimeMillis();
            logger.debug(InterfacesPlugin.Util.gs(
                    InterfacesPlugin.Event.DVCORE94427,
                    name, owner.getName(), owner.getId(), purpose
            ));
        }
    }

    // Log warning when acquisition attempt times out
    private void logTryTimeout(long timeoutMs) {
        Thread currentOwner = owner;
        String ownerName = (currentOwner != null ? currentOwner.getName() : "none");
        long ownerTid = (currentOwner != null ? currentOwner.getId() : -1L);
        logger.warn(InterfacesPlugin.Util.gs(
                InterfacesPlugin.Event.DVCORE94430,
                name, timeoutMs, ownerName, ownerTid
        ));
    }

    // ------------------------------------------------------------------------------------
    // Lock interface implementation (with identical logging semantics)
    // ------------------------------------------------------------------------------------

    @Override
    public void lock() {
        lock(inferPurpose());
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        lock.lockInterruptibly();
        afterAcquire(inferPurpose());
    }

    @Override
    public boolean tryLock() {
        boolean acquired = lock.tryLock();
        if (!acquired) return false;
        afterAcquire(inferPurpose());
        return true;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        boolean acquired = lock.tryLock(time, unit);
        if (!acquired) {
            logTryTimeout(unit.toMillis(time));
            return false;
        }
        afterAcquire(inferPurpose());
        return true;
    }

    @Override
    public void unlock() {
        long now = System.currentTimeMillis();

        // Will this unlock release the outermost acquisition (holdCount 1 -> 0)?
        final boolean releasingOutermost = (lock.getHoldCount() == 1);

        if (releasingOutermost) {
            final long totalHeldMs = now - firstAcquiredAtMs;

            // Snapshot owner to log consistent data before we null it out.
            final Thread thread = owner;
            final String ownerName = (thread != null ? thread.getName() : "none");
            final long ownerTid = (thread != null ? thread.getId() : -1L);

            if (totalHeldMs > warnThresholdMs) {
                logger.warn(InterfacesPlugin.Util.gs(
                        InterfacesPlugin.Event.DVCORE94429, // Lock held too long (total)
                        name,
                        totalHeldMs,
                        warnThresholdMs,
                        ownerName,
                        ownerTid,
                        inferPurpose()
                ));
            } else {
                logger.debug(InterfacesPlugin.Util.gs(
                        InterfacesPlugin.Event.DVCORE94428, // Lock released (total)
                        name,
                        totalHeldMs,
                        ownerName,
                        ownerTid,
                        inferPurpose()
                ));
            }

            owner = null;
            firstAcquiredAtMs = 0L;
        }

        lock.unlock();
    }

    @Override
    public Condition newCondition() {
        return lock.newCondition();
    }

    // ------------------------------------------------------------------------------------
    // Helpers (caller inference, formatting)
    // ------------------------------------------------------------------------------------

    private static StackWalker.StackFrame firstExternalFrame() {
        return CALLER_WALKER.walk(stream ->
                stream
                        .dropWhile(stackFrame -> stackFrame.getDeclaringClass() == ObservableLock.class)
                        .findFirst()
                        .orElse(null)
        );
    }

    /**
     * Best-effort purpose inferred from the first external caller frame.
     */
    private static String inferPurpose() {
        StackWalker.StackFrame stackFrame = firstExternalFrame();
        return (stackFrame != null) ? formatFrame(stackFrame) : "unknown";
    }

    /**
     * Use the creating class’ simple name to build a default lock name.
     */
    private static String inferCreatorClass() {
        StackWalker.StackFrame stackFrame = firstExternalFrame();
        if (stackFrame == null) return "Unknown";
        Class<?> c = stackFrame.getDeclaringClass();
        return (c != null ? c.getSimpleName() : "Unknown");
    }

    private static String formatFrame(StackWalker.StackFrame f) {
        return f.getClassName() + "#" + f.getMethodName() + ":" + f.getLineNumber();
    }

    // ------------------------------------------------------------------------------------
    // Diagnostics
    // ------------------------------------------------------------------------------------

    public String getName() {
        return name;
    }

    public long getWarnThresholdMs() {
        return warnThresholdMs;
    }

    public boolean isHeldByCurrentThread() {
        return lock.isHeldByCurrentThread();
    }

    public int getHoldCount() {
        return lock.getHoldCount();
    }

    public boolean isLocked() {
        return lock.isLocked();
    }

	public boolean hasQueuedThreads() {
		return lock.hasQueuedThreads();
	}

    public Thread getOwnerThread() {
        return owner;
    }

    // ------------------------------------------------------------------------------------
    // Guard (AutoCloseable)
    // ------------------------------------------------------------------------------------

    public final class Guard implements AutoCloseable {

        /**
         * Purpose for logging — either user-provided or inferred from the callsite.
         */
        private final String purpose;

        /**
         * Start timestamp when the guard was created / lock acquired.
         */
        private final long startedAtMs;

        /**
         * Snapshot of the thread that created the guard, for consistent logs.
         */
        private final String sectionOwnerName;
        private final long sectionOwnerTid;

        private Guard(String purpose, long startedAtMs, String sectionOwnerName, long sectionOwnerTid) {
            this.purpose = purpose;
            this.startedAtMs = startedAtMs;
            this.sectionOwnerName = sectionOwnerName;
            this.sectionOwnerTid = sectionOwnerTid;
        }

        @Override
        public void close() {
            long guardHeldMs = System.currentTimeMillis() - startedAtMs;

            // Log section release (not necessarily outermost), then delegate unlock().
            logger.debug(InterfacesPlugin.Util.gs(
                    InterfacesPlugin.Event.DVCORE94428,
                    name, guardHeldMs, sectionOwnerName, sectionOwnerTid, purpose
            ));
            unlock();
        }
    }
}