package net.siegeln.cameleer.saas.vendor;

import net.siegeln.cameleer.saas.audit.AuditAction;
import net.siegeln.cameleer.saas.audit.AuditService;
import net.siegeln.cameleer.saas.identity.LogtoConfig;
import net.siegeln.cameleer.saas.identity.LogtoManagementClient;
import net.siegeln.cameleer.saas.identity.ServerApiClient;
import net.siegeln.cameleer.saas.provisioning.ProvisioningProperties;
import net.siegeln.cameleer.saas.provisioning.TenantDatabaseService;
import net.siegeln.cameleer.saas.provisioning.TenantDataCleanupService;
import net.siegeln.cameleer.saas.identity.ServerApiClient.ServerHealthResponse;
import net.siegeln.cameleer.saas.license.LicenseEntity;
import net.siegeln.cameleer.saas.license.LicenseService;
import net.siegeln.cameleer.saas.provisioning.ProvisionResult;
import net.siegeln.cameleer.saas.provisioning.ServerStatus;
import net.siegeln.cameleer.saas.provisioning.TenantProvisionRequest;
import net.siegeln.cameleer.saas.provisioning.TenantProvisioner;
import net.siegeln.cameleer.saas.tenant.TenantEntity;
import net.siegeln.cameleer.saas.tenant.TenantRepository;
import net.siegeln.cameleer.saas.tenant.TenantService;
import net.siegeln.cameleer.saas.tenant.TenantStatus;
import net.siegeln.cameleer.saas.tenant.dto.CreateTenantRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

@Service
public class VendorTenantService {

    private static final Logger log = LoggerFactory.getLogger(VendorTenantService.class);
    private static final Duration DEFAULT_LICENSE_VALIDITY = Duration.ofDays(365);

    private final TenantService tenantService;
    private final TenantRepository tenantRepository;
    private final LicenseService licenseService;
    private final TenantProvisioner tenantProvisioner;
    private final ServerApiClient serverApiClient;
    private final LogtoManagementClient logtoClient;
    private final LogtoConfig logtoConfig;
    private final AuditService auditService;
    private final ProvisioningProperties provisioningProps;
    private final TenantDataCleanupService dataCleanupService;
    private final TenantDatabaseService tenantDatabaseService;
    private final VendorTenantService self;

    public VendorTenantService(TenantService tenantService,
                               TenantRepository tenantRepository,
                               LicenseService licenseService,
                               TenantProvisioner tenantProvisioner,
                               ServerApiClient serverApiClient,
                               LogtoManagementClient logtoClient,
                               LogtoConfig logtoConfig,
                               AuditService auditService,
                               ProvisioningProperties provisioningProps,
                               TenantDataCleanupService dataCleanupService,
                               TenantDatabaseService tenantDatabaseService,
                               @Lazy VendorTenantService self) {
        this.tenantService = tenantService;
        this.tenantRepository = tenantRepository;
        this.licenseService = licenseService;
        this.tenantProvisioner = tenantProvisioner;
        this.serverApiClient = serverApiClient;
        this.logtoClient = logtoClient;
        this.logtoConfig = logtoConfig;
        this.auditService = auditService;
        this.provisioningProps = provisioningProps;
        this.dataCleanupService = dataCleanupService;
        this.tenantDatabaseService = tenantDatabaseService;
        this.self = self;
    }

    @Transactional
    public TenantEntity createAndProvision(CreateTenantRequest request, UUID actorId) {
        // 1. Create tenant record (sets status = PROVISIONING) + Logto org
        TenantEntity tenant = tenantService.create(request, actorId);

        // 2. Create initial admin user in Logto org (if credentials provided)
        if (tenant.getLogtoOrgId() != null && logtoClient.isAvailable()) {
            String ownerRoleId = logtoClient.findOrgRoleIdByName("owner");

            // Create tenant admin
            if (request.adminUsername() != null && request.adminPassword() != null) {
                try {
                    logtoClient.createUserWithPassword(
                        request.adminUsername(), request.adminPassword(),
                        tenant.getLogtoOrgId(), ownerRoleId);
                } catch (Exception e) {
                    log.warn("Failed to create admin user for tenant {}: {}", tenant.getSlug(), e.getMessage());
                }
            }

            // Register OIDC redirect URIs for the tenant's server in the Traditional Web App
            String tradAppId = logtoConfig.getTradAppId();
            if (tradAppId != null) {
                String base = provisioningProps.publicProtocol() + "://" + provisioningProps.publicHost();
                String slug = tenant.getSlug();
                logtoClient.addAppRedirectUris(tradAppId,
                    List.of(base + "/t/" + slug + "/oidc/callback"),
                    List.of(base + "/t/" + slug, base + "/t/" + slug + "/login?local"));
            }
        }

        // 3. Generate license
        LicenseEntity license = licenseService.generateLicense(tenant, DEFAULT_LICENSE_VALIDITY, actorId);

        auditService.log(actorId, null, tenant.getId(),
                AuditAction.TENANT_CREATE, "provision:" + tenant.getSlug(),
                null, null, "SUCCESS", null);

        // 4. Provision server asynchronously AFTER transaction commits
        //    (the async thread needs the tenant row to be visible)
        if (tenantProvisioner.isAvailable()) {
            UUID tenantId = tenant.getId();
            String slug = tenant.getSlug();
            String tierName = tenant.getTier().name();
            String token = license.getToken();
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    self.provisionAsync(tenantId, slug, tierName, token, actorId);
                }
            });
        }

        return tenant;
    }

    @Async
    public void provisionAsync(UUID tenantId, String slug, String tier, String licenseToken, UUID actorId) {
        try {
            // Create per-tenant PG user + schema
            String dbPassword = java.util.UUID.randomUUID().toString().replace("-", "")
                    + java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8);
            try {
                tenantDatabaseService.createTenantDatabase(slug, dbPassword);
            } catch (Exception e) {
                log.error("Failed to create tenant database for {}: {}", slug, e.getMessage(), e);
                tenantRepository.findById(tenantId).ifPresent(t -> {
                    t.setProvisionError("Database setup failed: " + e.getMessage());
                    tenantRepository.save(t);
                });
                return;
            }

            // Store DB password on entity
            TenantEntity tenantForDb = tenantRepository.findById(tenantId).orElse(null);
            if (tenantForDb == null) {
                log.error("Tenant {} disappeared during provisioning", slug);
                return;
            }
            tenantForDb.setDbPassword(dbPassword);
            tenantRepository.save(tenantForDb);

            var provisionRequest = new TenantProvisionRequest(tenantId, slug, tier, licenseToken, dbPassword);
            ProvisionResult result = tenantProvisioner.provision(provisionRequest);

            TenantEntity tenant = tenantRepository.findById(tenantId).orElse(null);
            if (tenant == null) {
                log.error("Tenant {} disappeared during provisioning", slug);
                return;
            }

            if (result.success()) {
                tenant.setServerEndpoint(result.serverEndpoint());
                tenant.setProvisionError(null);
                tenant.setStatus(TenantStatus.ACTIVE);
                tenantRepository.save(tenant);

                // Push license to newly provisioned server
                try {
                    serverApiClient.pushLicense(result.serverEndpoint(), licenseToken);
                } catch (Exception e) {
                    log.warn("License push failed for tenant {}: {}", slug, e.getMessage());
                }

                // Configure OIDC on the provisioned server (SSO via Logto)
                if (logtoConfig.getTradAppId() != null && logtoConfig.getTradAppSecret() != null) {
                    try {
                        String publicBase = provisioningProps.publicProtocol() + "://" + provisioningProps.publicHost();
                        serverApiClient.pushOidcConfig(result.serverEndpoint(), Map.of(
                            "enabled", true,
                            "issuerUri", publicBase + "/oidc",
                            "clientId", logtoConfig.getTradAppId(),
                            "clientSecret", logtoConfig.getTradAppSecret(),
                            "autoSignup", true,
                            "defaultRoles", List.of("VIEWER"),
                            "displayNameClaim", "name",
                            "rolesClaim", "roles",
                            "audience", "https://api.cameleer.local"
                        ));
                        log.info("Pushed OIDC config to server for tenant {}", slug);
                    } catch (Exception e) {
                        log.warn("OIDC config push failed for tenant {}: {}", slug, e.getMessage());
                    }
                }

                log.info("Tenant {} provisioned successfully", slug);
            } else {
                tenant.setProvisionError(result.error());
                tenantRepository.save(tenant);
                log.error("Provisioning failed for tenant {}: {}", slug, result.error());
            }
        } catch (Exception e) {
            log.error("Unexpected error during async provisioning of tenant {}: {}", slug, e.getMessage(), e);
            tenantRepository.findById(tenantId).ifPresent(t -> {
                t.setProvisionError(e.getMessage());
                tenantRepository.save(t);
            });
        }
    }

    public ServerApiClient getServerApiClient() {
        return serverApiClient;
    }

    public List<TenantEntity> listAll() {
        return tenantService.findAll().stream()
            .filter(t -> t.getStatus() != TenantStatus.DELETED)
            .toList();
    }

    public Optional<TenantEntity> getById(UUID id) {
        return tenantService.getById(id);
    }

    public Optional<LicenseEntity> getLicenseForTenant(UUID tenantId) {
        return licenseService.getActiveLicense(tenantId);
    }

    public ServerStatus getServerStatus(TenantEntity tenant) {
        if (!tenantProvisioner.isAvailable()) {
            return ServerStatus.notFound();
        }
        return tenantProvisioner.getStatus(tenant.getSlug());
    }

    public ServerHealthResponse getServerHealth(TenantEntity tenant) {
        String endpoint = tenant.getServerEndpoint();
        if (endpoint == null || endpoint.isBlank()) {
            return new ServerHealthResponse(false, "NO_ENDPOINT");
        }
        return serverApiClient.getHealth(endpoint);
    }

    public void restartServer(UUID tenantId) {
        TenantEntity tenant = tenantService.getById(tenantId)
                .orElseThrow(() -> new IllegalArgumentException("Tenant not found"));
        if (!tenantProvisioner.isAvailable()) return;

        tenantProvisioner.stop(tenant.getSlug());
        try {
            tenantProvisioner.start(tenant.getSlug());
        } catch (RuntimeException e) {
            if (e.getMessage() != null && e.getMessage().contains("re-provision required")) {
                log.info("Containers missing for '{}' — re-provisioning", tenant.getSlug());
                tenantProvisioner.remove(tenant.getSlug());
                var license = licenseService.getActiveLicense(tenantId).orElse(null);
                String token = license != null ? license.getToken() : "";
                self.provisionAsync(tenantId, tenant.getSlug(), tenant.getTier().name(), token, null);
                return;
            }
            throw e;
        }
    }

    public void upgradeServer(UUID tenantId) {
        TenantEntity tenant = tenantService.getById(tenantId)
                .orElseThrow(() -> new IllegalArgumentException("Tenant not found"));
        if (!tenantProvisioner.isAvailable()) return;

        tenantProvisioner.upgrade(tenant.getSlug());

        // Re-provision with freshly pulled images
        var license = licenseService.getActiveLicense(tenantId).orElse(null);
        String token = license != null ? license.getToken() : "";
        self.provisionAsync(tenantId, tenant.getSlug(), tenant.getTier().name(), token, null);
    }

    @Transactional
    public TenantEntity suspend(UUID tenantId, UUID actorId) {
        TenantEntity tenant = tenantService.getById(tenantId)
                .orElseThrow(() -> new IllegalArgumentException("Tenant not found"));

        if (tenantProvisioner.isAvailable()) {
            try {
                tenantProvisioner.stop(tenant.getSlug());
            } catch (Exception e) {
                log.warn("Failed to stop containers for tenant {}: {}", tenant.getSlug(), e.getMessage());
            }
        }

        return tenantService.suspend(tenantId, actorId);
    }

    @Transactional
    public TenantEntity activate(UUID tenantId, UUID actorId) {
        TenantEntity tenant = tenantService.getById(tenantId)
                .orElseThrow(() -> new IllegalArgumentException("Tenant not found"));

        if (tenantProvisioner.isAvailable()) {
            try {
                tenantProvisioner.start(tenant.getSlug());
            } catch (Exception e) {
                log.warn("Failed to start containers for tenant {}: {}", tenant.getSlug(), e.getMessage());
            }
        }

        return tenantService.activate(tenantId, actorId);
    }

    @Transactional
    public void delete(UUID tenantId, UUID actorId) {
        TenantEntity tenant = tenantService.getById(tenantId)
                .orElseThrow(() -> new IllegalArgumentException("Tenant not found"));

        // Remove containers
        if (tenantProvisioner.isAvailable()) {
            try {
                tenantProvisioner.remove(tenant.getSlug());
            } catch (Exception e) {
                log.warn("Failed to remove containers for tenant {}: {}", tenant.getSlug(), e.getMessage());
            }
        }

        // Revoke license
        licenseService.revokeLicense(tenantId, actorId);

        // Delete Logto org
        if (logtoClient.isAvailable() && tenant.getLogtoOrgId() != null) {
            try {
                logtoClient.deleteOrganization(tenant.getLogtoOrgId());
            } catch (Exception e) {
                log.warn("Failed to delete Logto org for tenant {}: {}", tenant.getSlug(), e.getMessage());
            }
        }

        // Drop per-tenant PG user + database
        try {
            tenantDatabaseService.dropTenantDatabase(tenant.getSlug());
        } catch (Exception e) {
            log.warn("Failed to drop tenant database for {}: {}", tenant.getSlug(), e.getMessage());
        }

        // Erase tenant data from ClickHouse (GDPR)
        dataCleanupService.cleanupClickHouse(tenant.getSlug());

        // Soft-delete
        tenant.setStatus(TenantStatus.DELETED);
        tenantRepository.save(tenant);

        auditService.log(actorId, null, tenantId,
                AuditAction.TENANT_DELETE, tenant.getSlug(),
                null, null, "SUCCESS", null);
    }

    @Transactional
    public LicenseEntity renewLicense(UUID tenantId, UUID actorId) {
        TenantEntity tenant = tenantService.getById(tenantId)
                .orElseThrow(() -> new IllegalArgumentException("Tenant not found"));

        // Revoke current license
        licenseService.revokeLicense(tenantId, actorId);

        // Generate new license
        LicenseEntity newLicense = licenseService.generateLicense(tenant, DEFAULT_LICENSE_VALIDITY, actorId);

        // Push to server
        String endpoint = tenant.getServerEndpoint();
        if (endpoint != null && !endpoint.isBlank()) {
            try {
                serverApiClient.pushLicense(endpoint, newLicense.getToken());
            } catch (Exception e) {
                log.warn("Failed to push renewed license to server for tenant {}: {}", tenant.getSlug(), e.getMessage());
            }
        }

        return newLicense;
    }
}
