package de.gecheckt.pdf.umbenenner.application.service;

import java.util.Objects;

import org.json.JSONException;
import org.json.JSONObject;

import de.gecheckt.pdf.umbenenner.domain.model.AiRawResponse;
import de.gecheckt.pdf.umbenenner.domain.model.AiResponseParsingFailure;
import de.gecheckt.pdf.umbenenner.domain.model.AiResponseParsingResult;
import de.gecheckt.pdf.umbenenner.domain.model.AiResponseParsingSuccess;
import de.gecheckt.pdf.umbenenner.domain.model.ParsedAiResponse;

/**
 * Parses the raw AI response body into a structurally validated {@link ParsedAiResponse}.
 * <p>
 * This parser enforces the technical contract: the AI must provide exactly one
 * parseable JSON object containing the mandatory fields {@code title} and {@code reasoning},
 * and an optional {@code date} field. Surrounding free-text or Markdown code fences around
 * that JSON object are tolerated; the object is extracted before parsing.
 *
 * <h2>Parsing rules</h2>
 * <ul>
 *   <li>The response body must be a valid JSON object (no arrays, no primitives).</li>
 *   <li>{@code title} must be present and non-empty.</li>
 *   <li>{@code reasoning} must be present (may be empty in degenerate cases, but must exist).</li>
 *   <li>{@code date} is optional; if absent the field is modelled as an empty Optional.</li>
 *   <li>Additional JSON fields are tolerated and silently ignored.</li>
 *   <li>Leading or trailing free-text and Markdown code fences around the JSON object are
 *       tolerated: the first balanced JSON object is extracted and parsed. A response that
 *       contains no JSON object, or where an array (or other non-object) appears at the top
 *       level, is rejected as {@code NOT_JSON_OBJECT}.</li>
 * </ul>
 *
 * <h2>Architecture boundary</h2>
 * <p>
 * Only structural parsing is performed here. Semantic validation (title length,
 * special characters, date format, generic placeholder detection) is the responsibility
 * of {@link AiResponseValidator}.
 */
public final class AiResponseParser {
    private static final String JSON_KEY_TITLE = "title";
    private static final String JSON_KEY_REASONING = "reasoning";



    private AiResponseParser() {
        // Static utility – no instances
    }

    /**
     * Attempts to parse {@code rawResponse} into a {@link ParsedAiResponse}.
     * <p>
     * Returns {@link AiResponseParsingSuccess} if the response body is a valid JSON object
     * containing the mandatory fields. Returns {@link AiResponseParsingFailure} for any
     * structural problem: non-JSON content, JSON that is not an object, missing mandatory
     * fields, or extra free-text surrounding the JSON object.
     *
     * @param rawResponse the raw AI response body; must not be null
     * @return a parsing result indicating success or failure; never null
     * @throws NullPointerException if {@code rawResponse} is null
     */
    public static AiResponseParsingResult parse(AiRawResponse rawResponse) {
        Objects.requireNonNull(rawResponse, "rawResponse must not be null");

        String body = rawResponse.content();
        if (body == null || body.isBlank()) {
            return new AiResponseParsingFailure("EMPTY_RESPONSE", "AI response body is empty or blank");
        }

        // Extract the JSON object from the response even when it is surrounded by free-text
        // or Markdown code fences. Pure JSON-object responses are unaffected. A top-level
        // array (or other non-object structure) is still rejected.
        String jsonObject = extractJsonObject(body);
        if (jsonObject == null) {
            return new AiResponseParsingFailure(
                    "NOT_JSON_OBJECT",
                    "AI response is not a pure JSON object (contains extra text or is not an object)");
        }

        JSONObject json;
        try {
            json = new JSONObject(jsonObject);
        } catch (JSONException e) {
            return new AiResponseParsingFailure("INVALID_JSON", "AI response is not valid JSON: " + e.getMessage());
        }

        // Validate mandatory field: title
        if (!json.has(JSON_KEY_TITLE) || json.isNull(JSON_KEY_TITLE)) {
            return new AiResponseParsingFailure("MISSING_TITLE", "AI response missing mandatory field 'title'");
        }
        String title = json.getString(JSON_KEY_TITLE);
        if (title.isBlank()) {
            return new AiResponseParsingFailure("BLANK_TITLE", "AI response field 'title' is blank");
        }

        // Validate mandatory field: reasoning
        if (!json.has(JSON_KEY_REASONING) || json.isNull(JSON_KEY_REASONING)) {
            return new AiResponseParsingFailure("MISSING_REASONING", "AI response missing mandatory field 'reasoning'");
        }
        String reasoning = json.getString(JSON_KEY_REASONING);

        // Optional field: date
        String dateString = null;
        if (json.has("date") && !json.isNull("date")) {
            dateString = json.getString("date");
        }

        ParsedAiResponse parsed = ParsedAiResponse.of(title, reasoning, dateString);
        return new AiResponseParsingSuccess(parsed);
    }

    /**
     * Extracts the first complete JSON object from the response text.
     * <p>
     * Tolerates leading and trailing free-text as well as Markdown code fences
     * (e.g. {@code ```json … ```}) by reading from the first {@code '{'} up to its matching
     * {@code '}'}. Brace counting ignores curly braces inside JSON string literals and honours
     * escape sequences.
     * <p>
     * If a {@code '['} appears at the top level – before the first {@code '{'} – the response is
     * treated as an array (or other non-object structure) and {@code null} is returned.
     *
     * @param body the raw response body; must not be {@code null} or blank
     * @return the substring containing the JSON object, or {@code null} if no complete object was found
     */
    private static String extractJsonObject(String body) {
        int firstBrace = body.indexOf('{');
        if (firstBrace < 0) {
            return null;
        }
        int firstBracket = body.indexOf('[');
        if (firstBracket >= 0 && firstBracket < firstBrace) {
            // Array or other non-object structure at the top level.
            return null;
        }
        return balancedObject(body, firstBrace);
    }

    /**
     * Reads the balanced JSON object starting at {@code start} (the index of an opening {@code '{'}).
     * <p>
     * Counts curly braces while respecting string literals and escape sequences, and returns the
     * substring from the opening {@code '{'} up to and including its matching {@code '}'}.
     *
     * @param body  the text to scan
     * @param start the index of the opening {@code '{'}
     * @return the object substring including both braces, or {@code null} if no balanced object was found
     */
    private static String balancedObject(String body, int start) {
        boolean inString = false;
        boolean escaped = false;
        int depth = 0;
        for (int i = start; i < body.length(); i++) {
            char c = body.charAt(i);
            if (inString) {
                if (escaped) {
                    escaped = false;
                } else if (c == '\\') {
                    escaped = true;
                } else if (c == '"') {
                    inString = false;
                }
                continue;
            }
            switch (c) {
                case '"' -> inString = true;
                case '{' -> depth++;
                case '}' -> {
                    depth--;
                    if (depth == 0) {
                        return body.substring(start, i + 1);
                    }
                }
                default -> {
                    // ordinary character outside a string – ignored
                }
            }
        }
        return null;
    }
}
