package de.gecheckt.pdf.umbenenner.adapter.out.modelcatalog;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatalogPort;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogRequest;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult;

/**
 * Adapter implementing {@link AiModelCatalogPort} for the native Anthropic Claude provider family.
 * <p>
 * Fetches the list of available Claude models from the Anthropic models endpoint
 * ({@code GET {baseUrl}/v1/models}). Authentication uses the Anthropic-specific
 * {@code x-api-key} header together with the {@code anthropic-version} header.
 * <p>
 * <strong>Default base URL:</strong> When the request carries no base URL,
 * {@code https://api.anthropic.com} is used automatically.
 * <p>
 * <strong>Error handling:</strong> All expected error conditions (missing API key,
 * HTTP errors, timeouts, parse failures) are returned as specific
 * {@link ModelCatalogResult} sub-types. No exception is thrown to the caller.
 * <p>
 * <strong>Thread safety:</strong> This adapter is stateless. All configuration
 * values are read from the {@link ModelCatalogRequest} at call time. Multiple
 * threads may call {@link #fetchAvailableModels(ModelCatalogRequest)} concurrently
 * without synchronisation.
 * <p>
 * <strong>Non-goals:</strong>
 * <ul>
 *   <li>Retry logic — the caller is responsible for retry decisions.</li>
 *   <li>Caching — a fresh HTTP call is made on every invocation.</li>
 *   <li>Shared implementation with the OpenAI-compatible adapter.</li>
 * </ul>
 */
public class ClaudeModelCatalogAdapter implements AiModelCatalogPort {
    private static final String FAILURE_CODE_CONNECTION = "CONNECTION_FAILURE";
    private static final String FAILURE_CODE_UNKNOWN = "UNKNOWN";



    private static final Logger LOG = LogManager.getLogger(ClaudeModelCatalogAdapter.class);

    /** Anthropic models list endpoint path. */
    private static final String MODELS_ENDPOINT = "/v1/models";

    /** Default base URL for the Anthropic API, applied when the request carries no base URL. */
    static final String DEFAULT_BASE_URL = "https://api.anthropic.com";

    /** Anthropic-specific authentication header. */
    private static final String API_KEY_HEADER = "x-api-key";

    /** Required Anthropic API version header name. */
    private static final String ANTHROPIC_VERSION_HEADER = "anthropic-version";

    /** Required Anthropic API version header value. */
    private static final String ANTHROPIC_VERSION_VALUE = "2023-06-01";

    private static final String PROVIDER_ID = "claude";

    /**
     * Creates a new stateless Claude model catalogue adapter.
     * <p>
     * No configuration is held in the instance. All request parameters are
     * supplied per call via {@link ModelCatalogRequest}.
     */
    public ClaudeModelCatalogAdapter() {
        // stateless — no fields to initialise
    }

    /**
     * Fetches the list of available Claude models from the Anthropic models endpoint.
     * <p>
     * The adapter:
     * <ol>
     *   <li>Validates that the request carries a non-blank API key.</li>
     *   <li>Resolves the base URL (falls back to {@value #DEFAULT_BASE_URL}).</li>
     *   <li>Sends {@code GET {baseUrl}/v1/models} with Anthropic authentication headers.</li>
     *   <li>Maps HTTP 200 + non-empty {@code data} array to {@link ModelCatalogResult.Success}.</li>
     *   <li>Maps HTTP 200 + empty array to {@link ModelCatalogResult.EmptyList}.</li>
     *   <li>Maps HTTP 401 / 403 to {@link ModelCatalogResult.TechnicalFailure} with
     *       {@code AUTHENTICATION_FAILED}.</li>
     *   <li>Maps HTTP 404 to {@code ENDPOINT_NOT_FOUND}.</li>
     *   <li>Maps HTTP 5xx to {@code SERVER_ERROR}.</li>
     *   <li>Maps timeouts and connection failures to {@code CONNECTION_FAILURE}.</li>
     *   <li>Maps unparseable responses to {@code INVALID_RESPONSE}.</li>
     * </ol>
     *
     * @param request all values needed to contact the provider; must not be {@code null}
     * @return a non-{@code null} result encoding the outcome
     * @throws NullPointerException if {@code request} is {@code null}
     */
    @Override
    public ModelCatalogResult fetchAvailableModels(ModelCatalogRequest request) {
        java.util.Objects.requireNonNull(request, "request must not be null");

        if (request.apiKey().isEmpty() || request.apiKey().get().isBlank()) {
            LOG.warn("Claude model catalogue: API key is missing – cannot fetch model list");
            return new ModelCatalogResult.IncompleteConfiguration(PROVIDER_ID, "API-Schlüssel fehlt");
        }

        String apiKey = request.apiKey().get();
        String baseUrl = request.baseUrl()
                .filter(u -> !u.isBlank())
                .orElse(DEFAULT_BASE_URL);

        URI endpoint = buildEndpointUri(baseUrl);
        HttpClient httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(request.timeoutSeconds()))
                .build();

        LOG.info("Claude model catalogue: fetching models from {}", endpoint);
        try {
            HttpRequest httpRequest = HttpRequest.newBuilder(endpoint)
                    .header(API_KEY_HEADER, apiKey)
                    .header(ANTHROPIC_VERSION_HEADER, ANTHROPIC_VERSION_VALUE)
                    .GET()
                    .timeout(Duration.ofSeconds(request.timeoutSeconds()))
                    .build();

            HttpResponse<String> response = httpClient.send(httpRequest,
                    HttpResponse.BodyHandlers.ofString());

            return handleResponse(response);

        } catch (java.net.http.HttpTimeoutException e) {
            LOG.warn("Claude model catalogue: request timed out – {}", e.getMessage());
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
                    "Zeitüberschreitung beim Modellabruf: " + e.getMessage());
        } catch (java.net.ConnectException e) {
            LOG.warn("Claude model catalogue: connection failed – {}", e.getMessage());
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
                    "Verbindung zum Endpunkt fehlgeschlagen: " + e.getMessage());
        } catch (java.net.UnknownHostException e) {
            LOG.warn("Claude model catalogue: hostname not resolvable – {}", e.getMessage());
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
                    "Hostname nicht auflösbar: " + e.getMessage());
        } catch (java.io.IOException e) {
            LOG.warn("Claude model catalogue: IO error – {}", e.getMessage());
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
                    "E/A-Fehler beim Modellabruf: " + e.getMessage());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            LOG.warn("Claude model catalogue: request interrupted");
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
                    "Modellabruf wurde unterbrochen.");
        } catch (Exception e) {
            LOG.error("Claude model catalogue: unexpected error", e);
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_UNKNOWN,
                    "Unerwarteter Fehler: " + e.getMessage());
        }
    }

    /**
     * Maps the HTTP response to the appropriate {@link ModelCatalogResult} sub-type.
     *
     * @param response the HTTP response from the Anthropic models endpoint
     * @return the mapped result; never {@code null}
     */
    private ModelCatalogResult handleResponse(HttpResponse<String> response) {
        int status = response.statusCode();

        if (status == 401 || status == 403) {
            LOG.warn("Claude model catalogue: authentication failed – HTTP {}", status);
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "AUTHENTICATION_FAILED",
                    "Authentifizierung fehlgeschlagen (HTTP " + status + ")");
        }

        if (status == 404) {
            LOG.warn("Claude model catalogue: endpoint not found – HTTP 404");
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "ENDPOINT_NOT_FOUND",
                    "Endpunkt nicht gefunden (HTTP 404)");
        }

        if (status >= 500) {
            LOG.warn("Claude model catalogue: server error – HTTP {}", status);
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "SERVER_ERROR",
                    "Serverfehler beim Modellabruf (HTTP " + status + ")");
        }

        if (status != 200) {
            LOG.warn("Claude model catalogue: unexpected HTTP status {}", status);
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_UNKNOWN,
                    "Unerwarteter HTTP-Status: " + status);
        }

        return parseModelsResponse(response.body());
    }

    /**
     * Parses the JSON response body from the Anthropic models endpoint.
     * <p>
     * Expects a top-level {@code data} array where each element has an {@code id} field.
     *
     * @param responseBody the raw JSON response body
     * @return {@link ModelCatalogResult.Success} or {@link ModelCatalogResult.EmptyList}
     *         on parse success; {@link ModelCatalogResult.TechnicalFailure} with
     *         {@code INVALID_RESPONSE} on parse failure
     */
    private ModelCatalogResult parseModelsResponse(String responseBody) {
        try {
            JSONObject json = new JSONObject(responseBody);
            JSONArray dataArray = json.getJSONArray("data");

            List<String> modelIds = new ArrayList<>();
            for (int i = 0; i < dataArray.length(); i++) {
                JSONObject entry = dataArray.getJSONObject(i);
                String id = entry.optString("id", "").trim();
                if (!id.isEmpty()) {
                    modelIds.add(id);
                }
            }

            if (modelIds.isEmpty()) {
                LOG.warn("Claude model catalogue: provider returned empty model list");
                return new ModelCatalogResult.EmptyList(PROVIDER_ID, Instant.now());
            }

            LOG.info("Claude model catalogue: loaded {} model(s)", modelIds.size());
            return new ModelCatalogResult.Success(PROVIDER_ID, modelIds, Instant.now());

        } catch (JSONException e) {
            LOG.warn("Claude model catalogue: response could not be parsed – {}", e.getMessage());
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, "INVALID_RESPONSE",
                    "Antwort konnte nicht verarbeitet werden: " + e.getMessage());
        }
    }

    /**
     * Builds the full endpoint URI for the Anthropic models endpoint.
     *
     * @param baseUrl the resolved base URL (never blank)
     * @return the complete endpoint URI
     */
    private URI buildEndpointUri(String baseUrl) {
        URI base = URI.create(baseUrl);
        String path = base.getPath().replaceAll("/$", "") + MODELS_ENDPOINT;
        return URI.create(base.getScheme() + "://"
                + base.getHost()
                + (base.getPort() > 0 ? ":" + base.getPort() : "")
                + path);
    }

    /**
     * Package-private factory method for test injection of a custom {@link HttpClient}.
     * <p>
     * <strong>For testing only.</strong> Allows tests to provide a mock or stub client
     * without network access while exercising the full request-building and response-mapping
     * logic.
     *
     * @param request    the model catalogue request
     * @param httpClient the HTTP client to use instead of creating a new one
     * @return the mapped result
     */
    ModelCatalogResult fetchAvailableModelsWithClient(ModelCatalogRequest request, HttpClient httpClient) {
        java.util.Objects.requireNonNull(request, "request must not be null");
        java.util.Objects.requireNonNull(httpClient, "httpClient must not be null");

        if (request.apiKey().isEmpty() || request.apiKey().get().isBlank()) {
            LOG.warn("Claude model catalogue: API key is missing – cannot fetch model list");
            return new ModelCatalogResult.IncompleteConfiguration(PROVIDER_ID, "API-Schlüssel fehlt");
        }

        String apiKey = request.apiKey().get();
        String baseUrl = request.baseUrl()
                .filter(u -> !u.isBlank())
                .orElse(DEFAULT_BASE_URL);

        URI endpoint = buildEndpointUri(baseUrl);

        LOG.info("Claude model catalogue: fetching models from {} (test client)", endpoint);
        try {
            HttpRequest httpRequest = HttpRequest.newBuilder(endpoint)
                    .header(API_KEY_HEADER, apiKey)
                    .header(ANTHROPIC_VERSION_HEADER, ANTHROPIC_VERSION_VALUE)
                    .GET()
                    .timeout(Duration.ofSeconds(request.timeoutSeconds()))
                    .build();

            HttpResponse<String> response = httpClient.send(httpRequest,
                    HttpResponse.BodyHandlers.ofString());

            return handleResponse(response);

        } catch (java.net.http.HttpTimeoutException e) {
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
                    "Zeitüberschreitung beim Modellabruf: " + e.getMessage());
        } catch (java.net.ConnectException e) {
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
                    "Verbindung zum Endpunkt fehlgeschlagen: " + e.getMessage());
        } catch (java.net.UnknownHostException e) {
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
                    "Hostname nicht auflösbar: " + e.getMessage());
        } catch (java.io.IOException e) {
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
                    "E/A-Fehler beim Modellabruf: " + e.getMessage());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_CONNECTION,
                    "Modellabruf wurde unterbrochen.");
        } catch (Exception e) {
            LOG.error("Claude model catalogue: unexpected error", e);
            return new ModelCatalogResult.TechnicalFailure(PROVIDER_ID, FAILURE_CODE_UNKNOWN,
                    "Unerwarteter Fehler: " + e.getMessage());
        }
    }
}
