package com.cameleer.server.app.outbound;

import com.cameleer.server.app.license.LicenseEnforcer;
import com.cameleer.server.core.alerting.AlertRuleRepository;
import com.cameleer.server.core.outbound.OutboundConnection;
import com.cameleer.server.core.outbound.OutboundConnectionRepository;
import com.cameleer.server.core.outbound.OutboundConnectionService;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;

import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.List;
import java.util.UUID;

public class OutboundConnectionServiceImpl implements OutboundConnectionService {

    private final OutboundConnectionRepository repo;
    private final AlertRuleRepository ruleRepo;
    private final SsrfGuard ssrfGuard;
    private final LicenseEnforcer licenseEnforcer;
    private final String tenantId;

    public OutboundConnectionServiceImpl(
            OutboundConnectionRepository repo,
            AlertRuleRepository ruleRepo,
            SsrfGuard ssrfGuard,
            LicenseEnforcer licenseEnforcer,
            String tenantId) {
        this.repo = repo;
        this.ruleRepo = ruleRepo;
        this.ssrfGuard = ssrfGuard;
        this.licenseEnforcer = licenseEnforcer;
        this.tenantId = tenantId;
    }

    @Override
    public OutboundConnection create(OutboundConnection draft, String actingUserId) {
        licenseEnforcer.assertWithinCap("max_outbound_connections", repo.listByTenant(tenantId).size(), 1);
        assertNameUnique(draft.name(), null);
        validateUrl(draft.url());
        OutboundConnection c = new OutboundConnection(
                UUID.randomUUID(), tenantId, draft.name(), draft.description(),
                draft.url(), draft.method(), draft.defaultHeaders(), draft.defaultBodyTmpl(),
                draft.tlsTrustMode(), draft.tlsCaPemPaths(),
                draft.hmacSecretCiphertext(),
                draft.auth(), draft.allowedEnvironmentIds(),
                Instant.now(), actingUserId, Instant.now(), actingUserId);
        return repo.save(c);
    }

    @Override
    public OutboundConnection update(UUID id, OutboundConnection draft, String actingUserId) {
        OutboundConnection existing = repo.findById(tenantId, id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
        if (!existing.name().equals(draft.name())) {
            assertNameUnique(draft.name(), id);
        }
        validateUrl(draft.url());

        // Narrowing allowed-envs guard: if the new draft restricts to a non-empty set of envs,
        // find any envs that existed before but are absent in the draft.
        // Skip entirely if either side is empty (empty = "allowed in all envs").
        if (!existing.allowedEnvironmentIds().isEmpty() && !draft.allowedEnvironmentIds().isEmpty()) {
            List<UUID> removed = existing.allowedEnvironmentIds().stream()
                    .filter(e -> !draft.allowedEnvironmentIds().contains(e))
                    .toList();
            if (!removed.isEmpty()) {
                List<UUID> refs = rulesReferencing(id); // Plan 01 stub
                if (!refs.isEmpty()) {
                    throw new ResponseStatusException(HttpStatus.CONFLICT,
                        "Narrowing allowed environments while rules still reference this connection in removed envs: " + refs);
                }
            }
        }

        OutboundConnection updated = new OutboundConnection(
                id, tenantId, draft.name(), draft.description(), draft.url(), draft.method(),
                draft.defaultHeaders(), draft.defaultBodyTmpl(),
                draft.tlsTrustMode(), draft.tlsCaPemPaths(),
                // Retain existing secret if the draft omitted one (null = leave unchanged).
                draft.hmacSecretCiphertext() != null ? draft.hmacSecretCiphertext() : existing.hmacSecretCiphertext(),
                draft.auth(), draft.allowedEnvironmentIds(),
                existing.createdAt(), existing.createdBy(), Instant.now(), actingUserId);
        return repo.save(updated);
    }

    @Override
    public void delete(UUID id, String actingUserId) {
        List<UUID> refs = rulesReferencing(id); // Plan 01 stub
        if (!refs.isEmpty()) {
            throw new ResponseStatusException(HttpStatus.CONFLICT,
                "Outbound connection is referenced by rules: " + refs);
        }
        repo.delete(tenantId, id);
    }

    @Override
    public OutboundConnection get(UUID id) {
        return repo.findById(tenantId, id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    }

    @Override
    public List<OutboundConnection> list() {
        return repo.listByTenant(tenantId);
    }

    @Override
    public List<UUID> rulesReferencing(UUID id) {
        return ruleRepo.findRuleIdsByOutboundConnectionId(id);
    }

    private void assertNameUnique(String name, UUID excludingId) {
        repo.findByName(tenantId, name).ifPresent(c -> {
            if (!c.id().equals(excludingId)) {
                throw new ResponseStatusException(HttpStatus.CONFLICT, "Connection name already exists: " + name);
            }
        });
    }

    /**
     * Validate the webhook URL against SSRF pitfalls. Translates the guard's
     * {@link IllegalArgumentException} into a 400 Bad Request with the guard's
     * message preserved, so the client sees e.g. "private or loopback".
     */
    private void validateUrl(String url) {
        URI uri;
        try {
            uri = new URI(url);
        } catch (URISyntaxException e) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid URL: " + url);
        }
        try {
            ssrfGuard.validate(uri);
        } catch (IllegalArgumentException e) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage(), e);
        }
    }
}
