/*******************************************************************************
 * Copyright (C) 2026, Leo Galambos
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * 3. All advertising materials mentioning features or use of this software must
 *    display the following acknowledgement:
 *    This product includes software developed by the Egothor project.
 *
 * 4. Neither the name of the copyright holder nor the names of its contributors
 *    may be used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ******************************************************************************/
package zeroecho.pki.server;

import java.io.IOException;
import java.nio.file.Files;
import java.security.SecureRandom;
import java.time.Clock;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.concurrent.atomic.AtomicReference;

import zeroecho.pki.application.PkiSession;
import zeroecho.pki.application.PkiOperation;
import zeroecho.pki.application.PkiOperationOutcome;
import zeroecho.pki.application.PkiSessionRuntimeDependencies;
import zeroecho.pki.impl.fs.PosixTransactionalMetadataStore;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.bootstrap.PkiBootstrap;
import zeroecho.pki.server.acme.AcmeControlStore;

/**
 * Lifecycle owner for one server realm, one long-lived PKI session, and one
 * dedicated durable server-control authority.
 *
 * <p>The context is transport-neutral and synchronous. It creates no execution
 * lane, queue, thread, scheduler, retry loop, HTTP type, or ACME object. A future
 * transport can retain one instance for its process lifetime and submit existing
 * typed operations through {@link #gateway()}.</p>
 */
@SuppressWarnings({ "PMD.CommentDefaultAccessModifier", "PMD.CloseResource", "PMD.UseProperClassLoader",
        "PMD.AvoidCatchingGenericException", "PMD.LinguisticNaming", "PMD.ControlStatementBraces",
        "PMD.PreserveStackTrace", "PMD.SignatureDeclareThrowsException", "PMD.CommentRequired",
        "PMD.AvoidSynchronizedAtMethodLevel", "PMD.AvoidInstantiatingObjectsInLoops" })
public final class ServerRealmContext implements AutoCloseable {
    /** Closed lifecycle states. */
    public enum State { OPEN, CLOSING, CLOSED }

    private final ServerRealmConfiguration configuration;
    private final ServerControlStore control;
    private final PkiSession session;
    private final RoleTemplateCatalog roles;
    private final AuthorizationEngine authorization;
    private final ApprovalService approvals;
    private final BreakGlassService breakGlass;
    private final DisclosureService disclosure;
    private final RepositoryAliasService repositoryAliases;
    private final AcmeControlStore acmeControl;
    private final PublicRepositoryGateway publicRepository;
    private final AuditorViews auditorViews;
    private final ServerOperationGateway gateway;
    private final SafeAudit audit;
    private final AtomicReference<State> state = new AtomicReference<>(State.OPEN);

    private ServerRealmContext(ServerRealmConfiguration configuration, ServerControlStore control,
            PkiSession session, RoleTemplateCatalog roles, AuthorizationEngine authorization,
            ApprovalService approvals, BreakGlassService breakGlass, DisclosureService disclosure,
            RepositoryAliasService repositoryAliases,
            AuditorViews auditorViews, AuditSink auditSink, Clock clock) {
        this.configuration = configuration;
        this.control = control;
        this.session = session;
        this.roles = roles;
        this.authorization = authorization;
        this.approvals = approvals;
        this.breakGlass = breakGlass;
        this.disclosure = disclosure;
        this.repositoryAliases = repositoryAliases;
        this.acmeControl = new AcmeControlStore(control);
        this.acmeControl.validateAndRecover(clock);
        this.publicRepository = new PublicRepositoryGateway(configuration.realmId(), configuration.authorityExposure(),
                session.repository(), control, roles, authorization, breakGlass, disclosure, repositoryAliases,
                this::requireOpen);
        this.auditorViews = auditorViews;
        this.audit = new SafeAudit(clock, auditSink);
        this.gateway = new ServerOperationGateway(configuration.realmId(), configuration.authorityExposure(),
                control, roles, authorization, approvals, breakGlass, disclosure, auditorViews,
                Optional.of(repositoryAliases),
                new OperationSecurityDescriptors(),
                session.operations(), session.resourceScopes(), configuration.approvalPolicies(), clock, auditSink,
                this::requireOpen);
    }

    /**
     * Opens one production realm using explicit process-local capabilities.
     *
     * @param configuration validated immutable realm configuration
     * @param dependencies key-access capabilities, never secret values
     * @return fully recovered realm context
     */
    public static ServerRealmContext open(ServerRealmConfiguration configuration,
            PkiSessionRuntimeDependencies dependencies) {
        return open(configuration, dependencies, Clock.systemUTC(), new SecureRandom());
    }

    static ServerRealmContext open(ServerRealmConfiguration configuration,
            PkiSessionRuntimeDependencies dependencies, Clock clock, SecureRandom random) {
        ServerRealmConfiguration exact = Objects.requireNonNull(configuration, "configuration");
        PkiSessionRuntimeDependencies runtime = Objects.requireNonNull(dependencies, "dependencies");
        Objects.requireNonNull(clock, "clock");
        Objects.requireNonNull(random, "random");
        if (Files.isSymbolicLink(exact.controlLogPath())) {
            throw new IllegalArgumentException("Server-control log must not be a symbolic link");
        }
        ServerControlStore control = null;
        SharedAuditSink audit = null;
        PkiSession session = null;
        try {
            control = new ServerControlStore(openControl(exact));
            ServerControlStore.RealmRecord realm = new ServerControlStore.RealmRecord(exact.realmId(),
                    exact.displayName(), exact.authorityExposure(), exact.authorizationCommitment(),
                    exact.approvalCommitment(), exact.disclosureCommitment(), exact.controlStoreId());
            control.ensureRealm(realm);
            RoleTemplateCatalog roles = RoleTemplateCatalog.load(ServerRealmContext.class.getClassLoader());
            control.validateAll();
            control.validateReferences(roles);
            audit = new SharedAuditSink(PkiBootstrap.openAudit(exact.pkiSessionConfiguration().audit()));
            session = PkiSession.open(exact.pkiSessionConfiguration(), runtime.withAuditSink(audit));
            validateExposure(exact.authorityExposure(), session);
            AuthorizationEngine authorization = new AuthorizationEngine(clock);
            ApprovalService approvals = new ApprovalService(control, clock, audit);
            BreakGlassService breakGlass = new BreakGlassService(control, clock, audit);
            DisclosureService disclosure = new DisclosureService(exact.realmId(), control,
                    exact.disclosureDefaults(), clock, random, audit);
            RepositoryAliasService aliases = new RepositoryAliasService(exact.realmId(), control,
                    session.repository(), disclosure, clock, audit);
            AuditorViews views = new AuditorViews(clock, audit);
            ServerRealmContext result = new ServerRealmContext(exact, control, session, roles, authorization,
                    approvals, breakGlass, disclosure, aliases, views, audit, clock);
            result.audit.record("REALM_OPEN", "system", Optional.empty(), Map.of("realm", exact.realmId().value()));
            return result;
        } catch (RuntimeException | Error primary) {
            closeAfterFailure(session, audit, control, primary);
            throw primary;
        }
    }

    /** @return exact realm configuration without rendering provider values */
    public ServerRealmConfiguration configuration() { requireOpen(); return configuration; }
    /** @return the one long-lived PKI session */
    public PkiSession session() { requireOpen(); return session; }
    /** @return immutable built-in role template catalog */
    public RoleTemplateCatalog roleTemplates() { requireOpen(); return roles; }
    /** @return deterministic authorization engine */
    public AuthorizationEngine authorization() { requireOpen(); return authorization; }
    /** @return durable approval service */
    public ApprovalService approvals() { requireOpen(); return approvals; }
    /** @return durable break-glass service */
    public BreakGlassService breakGlass() { requireOpen(); return breakGlass; }
    /** @return durable disclosure decision service */
    public DisclosureService disclosure() { requireOpen(); return disclosure; }
    /** @return durable non-authoritative public repository alias service */
    public RepositoryAliasService repositoryAliases() { requireOpen(); return repositoryAliases; }
    /** @return typed ACME records in the realm's sole durable control authority */
    public AcmeControlStore acmeControl() { requireOpen(); return acmeControl; }
    /** @return read-only disclosed public repository gateway */
    public PublicRepositoryGateway publicRepository() { requireOpen(); return publicRepository; }
    /** @return explicit auditor projection service */
    public AuditorViews auditorViews() { requireOpen(); return auditorViews; }
    /** @return authorized typed-operation gateway */
    public ServerOperationGateway gateway() { requireOpen(); return gateway; }
    /**
     * Resolves one persisted principal for transport authentication.
     *
     * @param principalId canonical principal identity
     * @return persisted identity metadata without credentials or grants
     * @throws IllegalArgumentException when the principal is unavailable
     * @throws IllegalStateException when this realm is not open
     */
    public SecurityPrincipal principal(String principalId) {
        requireOpen();
        return control.requirePrincipal(principalId);
    }
    /**
     * Records one transport-safe lifecycle or request classification through the
     * shared realm audit authority.
     *
     * @param action stable transport action
     * @param principalId authenticated principal or {@code system}
     * @param safeDetails finite pre-redacted details
     */
    public void auditTransport(String action, String principalId, Map<String, String> safeDetails) {
        requireOpen();
        Permission.requireBounded(action, 128, "audit action");
        Permission.requirePrincipal(principalId);
        audit.record(action, principalId, Optional.empty(), Map.copyOf(safeDetails));
    }
    /** @return current lifecycle state */
    public State state() { return state.get(); }

    /** Creates a principal and records only safe administrative audit metadata. */
    public void createPrincipal(SecurityPrincipal principal, String actorPrincipalId) {
        requireOpen();
        control.createPrincipal(principal);
        audit.record("PRINCIPAL_CREATE", actorPrincipalId, Optional.empty(),
                Map.of("principal", principal.principalId(), "enabled", Boolean.toString(principal.enabled())));
    }

    /** Changes only the enabled state of an existing principal. */
    public SecurityPrincipal setPrincipalEnabled(String principalId, boolean enabled, String actorPrincipalId) {
        requireOpen();
        SecurityPrincipal current = control.requirePrincipal(principalId);
        SecurityPrincipal updated = new SecurityPrincipal(current.principalId(), current.type(),
                current.displayName(), current.organization(), current.attributes(), enabled);
        control.replacePrincipal(current, updated);
        audit.record("PRINCIPAL_STATE", actorPrincipalId, Optional.empty(),
                Map.of("principal", principalId, "enabled", Boolean.toString(enabled)));
        return updated;
    }

    /** Creates one scoped role assignment; templates remain non-authoritative alone. */
    public void assignRole(RoleTemplateCatalog.Assignment assignment, String actorPrincipalId) {
        requireOpen();
        roles.instantiate(assignment);
        control.requirePrincipal(assignment.principalId());
        control.createAssignment(assignment);
        audit.record("ROLE_ASSIGNMENT_CREATE", actorPrincipalId, Optional.empty(),
                Map.of("assignment", assignment.assignmentId(), "template", assignment.templateId()));
    }

    /** Creates one explicit scoped direct grant. */
    public void grant(Permission.Grant grant, String actorPrincipalId) {
        requireOpen();
        control.requirePrincipal(grant.principalId());
        control.createGrant(grant);
        audit.record("PERMISSION_GRANT_CREATE", actorPrincipalId, Optional.empty(),
                Map.of("grant", grant.grantId(), "effect", grant.effect().name()));
    }

    /**
     * Rejects new work and closes session then server-control authority. Repeated
     * calls are harmless and failure suppression preserves causal order.
     */
    @Override
    public void close() throws Exception {
        if (!state.compareAndSet(State.OPEN, State.CLOSING)) return;
        Throwable primary = null;
        try {
            audit.record("REALM_CLOSE", "system", Optional.empty(), Map.of("realm", configuration.realmId().value()));
        } catch (Throwable failure) {
            primary = failure;
        }
        primary = closeOne(session, primary);
        primary = closeOne(control, primary);
        state.set(State.CLOSED);
        rethrow(primary);
    }

    private void requireOpen() {
        if (state.get() != State.OPEN) throw new IllegalStateException("Server realm is not open");
    }

    private static PosixTransactionalMetadataStore openControl(ServerRealmConfiguration configuration) {
        try {
            if (Files.exists(configuration.controlLogPath())) {
                PosixTransactionalMetadataStore opened = PosixTransactionalMetadataStore.open(
                        configuration.controlLogPath(), OptionalLong.of(1_048_576));
                if (!opened.id().equals(configuration.controlStoreId())) {
                    opened.close();
                    throw new IllegalStateException("Server-control store identity differs");
                }
                return opened;
            }
            return PosixTransactionalMetadataStore.create(configuration.controlLogPath(),
                    configuration.controlStoreId(), OptionalLong.of(1_048_576));
        } catch (IOException failure) {
            throw new IllegalStateException("Server-control metadata authority cannot be opened");
        }
    }

    private static void validateExposure(AuthorityExposurePolicy exposure, PkiSession session) {
        if (exposure.mode() == AuthorityExposurePolicy.Mode.EXPLICIT_AUTHORITIES) {
            for (zeroecho.pki.api.PkiId authorityId : exposure.authorityIds()) {
                PkiOperationOutcome outcome = session.operations().execute(
                        new PkiOperation.InspectAuthority(authorityId), zeroecho.core.io.CancellationSignal.NONE);
                if (!(outcome instanceof PkiOperationOutcome.Success)) {
                    throw new IllegalStateException("Configured authority exposure contains an unavailable authority");
                }
            }
        }
    }

    private static void closeAfterFailure(PkiSession session, AuditSink audit, ServerControlStore control,
            Throwable primary) {
        Throwable result = closeOne(session, primary);
        if (session == null) result = closeOne(audit, result);
        closeOne(control, result);
    }

    private static Throwable closeOne(AutoCloseable resource, Throwable primary) {
        if (resource == null) return primary;
        try {
            resource.close();
        } catch (Throwable failure) {
            if (primary == null) return failure;
            primary.addSuppressed(failure);
        }
        return primary;
    }

    private static void rethrow(Throwable failure) throws Exception {
        if (failure == null) return;
        if (failure instanceof Exception exception) throw exception;
        if (failure instanceof Error error) throw error;
        throw new IllegalStateException("Unexpected realm close failure");
    }

    private static final class SharedAuditSink implements AuditSink {
        private final AuditSink delegate;
        private boolean closed;

        private SharedAuditSink(AuditSink delegate) { this.delegate = Objects.requireNonNull(delegate, "delegate"); }
        @Override public synchronized void record(zeroecho.pki.api.audit.AuditEvent event) {
            if (closed) throw new IllegalStateException("Audit sink is closed");
            delegate.record(event);
        }
        @Override public synchronized void close() {
            if (!closed) { closed = true; delegate.close(); }
        }
    }
}
