package cn.yunzhixue.ability.center.examsprint.infrastructure.report.storage;

import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
import com.azure.core.exception.HttpResponseException;
import com.azure.core.http.HttpHeaderName;
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.rest.Response;
import com.azure.core.util.Context;
import com.azure.storage.blob.BlobClient;
import com.azure.storage.blob.BlobContainerClient;
import com.azure.storage.blob.BlobContainerClientBuilder;
import com.azure.storage.blob.models.BlobHttpHeaders;
import com.azure.storage.blob.models.BlobStorageException;
import com.azure.storage.blob.models.BlockBlobItem;
import com.azure.storage.blob.options.BlobParallelUploadOptions;
import com.azure.storage.common.StorageSharedKeyCredential;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

@Component
@ConditionalOnProperty(prefix = "ability.exam-sprint.report.storage", name = "type", havingValue = "azure")
public class AzureBlobExamSprintReportStorage implements ExamSprintReportStorage {

    private static final Logger log = LoggerFactory.getLogger(AzureBlobExamSprintReportStorage.class);

    private final BlobContainerClient containerClient;
    private final String containerName;
    private final String downloadUrlPrefix;
    private final Clock clock;

    @Autowired
    public AzureBlobExamSprintReportStorage(
            @Value("${ability.exam-sprint.report.storage.container-name:reports}") String containerName,
            @Value("${ability.exam-sprint.report.storage.connection-string:}") String connectionString,
            @Value("${ability.exam-sprint.report.storage.endpoint:}") String endpoint,
            @Value("${ability.exam-sprint.report.storage.account-name:}") String accountName,
            @Value("${ability.exam-sprint.report.storage.account-key:}") String accountKey,
            @Value("${ability.exam-sprint.report.storage.download-url-prefix:}") String downloadUrlPrefix,
            Clock clock) {
        this(buildContainerClient(containerName, connectionString, endpoint, accountName, accountKey),
                containerName,
                downloadUrlPrefix,
                clock);
        this.containerClient.createIfNotExists();
    }

    AzureBlobExamSprintReportStorage(BlobContainerClient containerClient, String containerName, String downloadUrlPrefix, Clock clock) {
        this.containerClient = containerClient;
        this.containerName = normalizeSegment(containerName, "containerName");
        this.downloadUrlPrefix = normalizeDownloadUrlPrefix(downloadUrlPrefix);
        this.clock = clock;
    }

    @Override
    public StoredExamSprintReportFile upload(
            String reportId,
            ReportType reportType,
            String fileName,
            byte[] pdfBytes,
            Instant expiresAt) {
        long uploadStartedNanos = System.nanoTime();
        String blobName = normalizeSegment(fileName, "fileName");
        BlobClient blobClient = containerClient.getBlobClient(blobName);
        log.info(
                "exam_sprint_report_azure_storage_upload_stage_completed reportId={} reportType={} stage=client_resolved storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={}",
                reportId,
                reportType,
                blobName,
                blobName,
                fileName,
                pdfBytes.length,
                elapsedMillis(uploadStartedNanos));

        Map<String, String> metadata = Map.of(
                "reportId", reportId,
                "reportType", reportType.name(),
                "expiresAt", expiresAt.toString(),
                "uploadedAt", clock.instant().toString());
        BlobParallelUploadOptions options = new BlobParallelUploadOptions(
                new ByteArrayInputStream(pdfBytes),
                pdfBytes.length)
                .setHeaders(new BlobHttpHeaders().setContentType("application/pdf"))
                .setMetadata(metadata);
        long uploadRequestStartedNanos = System.nanoTime();
        Response<BlockBlobItem> uploadResponse;
        try {
            uploadResponse = blobClient.uploadWithResponse(options, Context.NONE);
        } catch (RuntimeException e) {
            logUploadFailure(reportId, reportType, fileName, pdfBytes.length, blobName, uploadRequestStartedNanos, e);
            throw e;
        }
        log.info(
                "exam_sprint_report_azure_storage_upload_stage_completed reportId={} reportType={} stage=upload_request_completed storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={} azureStatusCode={} azureRequestId={} azureClientRequestId={} azureETag={} azureLastModified={}",
                reportId,
                reportType,
                blobName,
                blobName,
                fileName,
                pdfBytes.length,
                elapsedMillis(uploadRequestStartedNanos),
                uploadResponse.getStatusCode(),
                headerValue(uploadResponse.getHeaders(), HttpHeaderName.X_MS_REQUEST_ID),
                headerValue(uploadResponse.getHeaders(), HttpHeaderName.X_MS_CLIENT_REQUEST_ID),
                uploadResponse.getValue() == null ? null : uploadResponse.getValue().getETag(),
                uploadResponse.getValue() == null ? null : uploadResponse.getValue().getLastModified());
        log.info(
                "exam_sprint_report_azure_storage_upload_completed reportId={} reportType={} storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={}",
                reportId,
                reportType,
                blobName,
                blobName,
                fileName,
                pdfBytes.length,
                elapsedMillis(uploadStartedNanos));
        return new StoredExamSprintReportFile(blobName, fileName);
    }

    @Override
    public URI generateDownloadUrl(String storageObjectKey, Duration ttl) {
        // This implementation returns a public Blob URL, so ttl is not used.
        String normalizedStorageObjectKey = normalizeSegment(storageObjectKey, "storageObjectKey");
        return URI.create(downloadUrlPrefix + "/" + containerName + "/" + encodePathSegments(normalizedStorageObjectKey));
    }

    @Override
    public Optional<StoredExamSprintReportContent> download(String storageObjectKey) {
        BlobClient blobClient = containerClient.getBlobClient(storageObjectKey);
        if (!blobClient.exists()) {
            return Optional.empty();
        }

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        blobClient.downloadStream(outputStream);
        String contentType = blobClient.getProperties().getContentType();
        return Optional.of(new StoredExamSprintReportContent(
                fileName(storageObjectKey),
                outputStream.toByteArray(),
                contentType == null || contentType.isBlank() ? "application/pdf" : contentType));
    }

    @Override
    public void delete(String storageObjectKey) {
        containerClient.getBlobClient(storageObjectKey).deleteIfExists();
    }

    private String fileName(String storageObjectKey) {
        return storageObjectKey.substring(storageObjectKey.lastIndexOf('/') + 1);
    }

    private static String normalizeDownloadUrlPrefix(String value) {
        if (!hasText(value)) {
            throw new IllegalStateException("Azure storage download-url-prefix is incomplete");
        }
        String normalized = value.trim();
        while (normalized.endsWith("/")) {
            normalized = normalized.substring(0, normalized.length() - 1);
        }
        if (!hasText(normalized)) {
            throw new IllegalStateException("Azure storage download-url-prefix is incomplete");
        }
        return normalized;
    }

    private static String normalizeSegment(String value, String name) {
        String normalized = trimSlashes(value);
        if (!hasText(normalized)) {
            throw new IllegalStateException("Azure storage " + name + " is incomplete");
        }
        return normalized;
    }

    private static String encodePathSegments(String value) {
        try {
            return new URI(null, null, value, null).getRawPath();
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException("Azure storage storageObjectKey is not a valid path", e);
        }
    }

    private static String trimSlashes(String value) {
        if (value == null) {
            return "";
        }
        String normalized = value.trim();
        while (normalized.startsWith("/")) {
            normalized = normalized.substring(1);
        }
        while (normalized.endsWith("/")) {
            normalized = normalized.substring(0, normalized.length() - 1);
        }
        return normalized;
    }

    private static BlobContainerClient buildContainerClient(
            String containerName,
            String connectionString,
            String endpoint,
            String accountName,
            String accountKey) {
        String normalizedContainerName = normalizeSegment(containerName, "containerName");
        BlobContainerClientBuilder builder = new BlobContainerClientBuilder().containerName(normalizedContainerName);

        if (hasText(connectionString)) {
            builder.connectionString(connectionString);
        } else if (hasText(endpoint) && hasText(accountName) && hasText(accountKey)) {
            builder.endpoint(endpoint)
                    .credential(new StorageSharedKeyCredential(accountName, accountKey));
        } else {
            throw new IllegalStateException("Azure storage configuration is incomplete");
        }

        return builder.buildClient();
    }

    private void logUploadFailure(
            String reportId,
            ReportType reportType,
            String fileName,
            int pdfByteLength,
            String blobName,
            long uploadRequestStartedNanos,
            RuntimeException exception) {
        HttpHeaders headers = responseHeaders(exception);
        log.warn(
                "exam_sprint_report_azure_storage_upload_failed reportId={} reportType={} storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={} exceptionType={} azureStatusCode={} azureRequestId={} azureClientRequestId={} azureErrorCode={}",
                reportId,
                reportType,
                blobName,
                blobName,
                fileName,
                pdfByteLength,
                elapsedMillis(uploadRequestStartedNanos),
                exception.getClass().getSimpleName(),
                azureStatusCode(exception),
                headerValue(headers, HttpHeaderName.X_MS_REQUEST_ID),
                headerValue(headers, HttpHeaderName.X_MS_CLIENT_REQUEST_ID),
                azureErrorCode(exception));
    }

    private static Integer azureStatusCode(RuntimeException exception) {
        if (exception instanceof BlobStorageException blobStorageException) {
            return blobStorageException.getStatusCode();
        }
        if (exception instanceof HttpResponseException httpResponseException && httpResponseException.getResponse() != null) {
            return httpResponseException.getResponse().getStatusCode();
        }
        return null;
    }

    private static String azureErrorCode(RuntimeException exception) {
        if (exception instanceof BlobStorageException blobStorageException && blobStorageException.getErrorCode() != null) {
            return blobStorageException.getErrorCode().toString();
        }
        HttpHeaders headers = responseHeaders(exception);
        return headerValue(headers, "x-ms-error-code");
    }

    private static HttpHeaders responseHeaders(RuntimeException exception) {
        if (exception instanceof HttpResponseException httpResponseException && httpResponseException.getResponse() != null) {
            return httpResponseException.getResponse().getHeaders();
        }
        return null;
    }

    private static String headerValue(HttpHeaders headers, HttpHeaderName headerName) {
        return headers == null ? null : headers.getValue(headerName);
    }

    private static String headerValue(HttpHeaders headers, String headerName) {
        return headers == null ? null : headers.getValue(headerName);
    }

    private static boolean hasText(String value) {
        return value != null && !value.isBlank();
    }

    private long elapsedMillis(long startedNanos) {
        return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedNanos);
    }
}
