using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;

namespace JebAI
{
    public class ChatMessage
    {
        public string Role { get; set; }
        public string Content { get; set; }
    }

    [Serializable]
    internal class OpenAICompatMessage
    {
        public string role;
        public string content;
    }

    [Serializable]
    internal class OpenAICompatResponse
    {
        public OpenAICompatChoice[] choices;
        public OpenAICompatError error;
        public string errorText;
    }

    [Serializable]
    internal class OpenAICompatChoice
    {
        public OpenAICompatMessage message;
    }

    [Serializable]
    internal class OpenAICompatError
    {
        public string message;
    }

    public static class OpenAICompatClient
    {
        public static int BuildMessageCount(
            string systemPrompt,
            List<ChatMessage> history,
            string userMessage)
        {
            int count = 1;

            if (history != null)
                count += history.Count;

            if (!string.IsNullOrEmpty(userMessage))
                count++;

            return count;
        }

        public static string BuildRequestBody(
            string systemPrompt,
            List<ChatMessage> history,
            string userMessage,
            int maxTokens = 150)
        {
            var cfg = JebAISettings.Instance;
            string systemContent = systemPrompt ?? string.Empty;
            if (cfg.DisableModelThinking)
            {
                string directive = string.IsNullOrWhiteSpace(cfg.NoThinkDirective)
                    ? "/no_think"
                    : cfg.NoThinkDirective.Trim();

                if (!systemContent.StartsWith(directive, StringComparison.Ordinal))
                    systemContent = directive + "\n" + systemContent;
            }

            var messages = new List<OpenAICompatMessage>
            {
                new OpenAICompatMessage { role = "system", content = systemContent }
            };

            if (history != null)
            {
                foreach (ChatMessage message in history)
                {
                    messages.Add(new OpenAICompatMessage
                    {
                        role = message.Role,
                        content = message.Content,
                    });
                }
            }

            if (!string.IsNullOrEmpty(userMessage))
            {
                messages.Add(new OpenAICompatMessage
                {
                    role = "user",
                    content = userMessage,
                });
            }

            var body = new StringBuilder(512 + messages.Count * 128);
            body.Append('{');
            body.Append("\"model\":\"").Append(EscapeJson(cfg.ModelName)).Append("\",");
            body.Append("\"messages\":[");

            for (int i = 0; i < messages.Count; i++)
            {
                if (i > 0) body.Append(',');

                OpenAICompatMessage message = messages[i];
                body.Append('{');
                body.Append("\"role\":\"").Append(EscapeJson(message.role)).Append("\",");
                body.Append("\"content\":\"").Append(EscapeJson(message.content)).Append("\"");
                body.Append('}');
            }

            body.Append("],");
            body.Append("\"max_tokens\":").Append(Mathf.Max(1, maxTokens)).Append(',');
            body.Append("\"temperature\":").Append(cfg.Temperature.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture)).Append(',');
            body.Append("\"stream\":false");
            if (cfg.DisableModelThinking)
            {
                // Llama/Qwen-style OpenAI-compatible servers commonly honor this field.
                body.Append(",\"chat_template_kwargs\":{\"enable_thinking\":false}");
            }
            body.Append('}');
            return body.ToString();
        }

        public static bool TryParseResponse(
            string raw,
            out string reply,
            out string error)
        {
            reply = null;
            error = null;

            if (string.IsNullOrWhiteSpace(raw))
            {
                error = "(empty body)";
                return false;
            }

            if (TryExtractReplyFallback(raw, out reply))
            {
                reply = SanitizeModelText(reply);
                if (!string.IsNullOrWhiteSpace(reply))
                    return true;
            }

            try
            {
                OpenAICompatResponse parsed = JsonUtility.FromJson<OpenAICompatResponse>(raw);
                if (!string.IsNullOrEmpty(parsed?.errorText))
                {
                    error = parsed.errorText;
                }
                else if (parsed?.choices == null || parsed.choices.Length == 0)
                {
                    if (TryExtractReasoningContent(raw, out _))
                        error = "OpenAI-compatible server returned reasoning content but no parseable visible choice content.";
                    else
                        error = "OpenAI-compatible server returned an empty choices array.";
                }
                else
                {
                    reply = SanitizeModelText(parsed.choices[0]?.message?.content);
                    if (!string.IsNullOrEmpty(reply))
                        return true;

                    if (TryExtractReasoningContent(raw, out _))
                    {
                        error = "OpenAI-compatible server returned reasoning content but no visible reply text (likely token budget exhausted by reasoning).";
                    }
                    else
                    {
                        error = "OpenAI-compatible server returned a blank message content.";
                    }
                }
            }
            catch (ArgumentException e)
            {
                error = $"Failed to parse OpenAI-compatible response: {e.Message}";
            }
            catch (Exception e)
            {
                error = $"Unexpected parse error: {e.Message}";
            }

            if (TryExtractReplyFallback(raw, out reply))
            {
                reply = SanitizeModelText(reply);
                error = null;
                return !string.IsNullOrWhiteSpace(reply);
            }

            if (TryDeriveReplyFromReasoning(raw, out reply))
            {
                error = null;
                return true;
            }

            return false;
        }

        public static string TryExtractError(string raw)
        {
            if (string.IsNullOrWhiteSpace(raw)) return "(empty body)";

            try
            {
                OpenAICompatResponse errObj = JsonUtility.FromJson<OpenAICompatResponse>(raw);
                if (!string.IsNullOrEmpty(errObj?.error?.message))
                    return errObj.error.message;
                if (!string.IsNullOrEmpty(errObj?.errorText))
                    return errObj.errorText;
            }
            catch
            {
            }

            string inlineError = TryExtractInlineString(raw, "\"error\":\"");
            if (!string.IsNullOrEmpty(inlineError))
                return inlineError;

            return raw.Length > 300 ? raw.Substring(0, 300) + "..." : raw;
        }

        public static bool LooksReasoningOnlyResponse(string raw)
        {
            if (string.IsNullOrWhiteSpace(raw)) return false;
            if (!TryExtractReasoningContent(raw, out _)) return false;
            return !TryExtractReplyFallback(raw, out _);
        }

        public static string ClampReplyForInteraction(string rawReply, InteractionType type)
        {
            string text = SanitizeModelText(rawReply);
            if (string.IsNullOrWhiteSpace(text)) return null;

            int maxSentences;
            int maxChars;

            switch (type)
            {
                case InteractionType.Poke:
                case InteractionType.StagingReaction:
                    maxSentences = 1;
                    maxChars = 110;
                    break;

                case InteractionType.EventReaction:
                case InteractionType.AskFears:
                case InteractionType.AskDreams:
                case InteractionType.AskMission:
                case InteractionType.AskCrewmate:
                    maxSentences = 2;
                    maxChars = 170;
                    break;

                case InteractionType.Chat:
                    maxSentences = 2;
                    maxChars = 220;
                    break;

                case InteractionType.Speak:
                default:
                    maxSentences = 2;
                    maxChars = 180;
                    break;
            }

            text = CollapseWhitespace(text);
            text = TakeFirstSentences(text, maxSentences);
            text = TrimToWordBoundary(text, maxChars);
            return text;
        }

        private static string EscapeJson(string value)
        {
            if (string.IsNullOrEmpty(value)) return string.Empty;

            var escaped = new StringBuilder(value.Length + 16);
            for (int i = 0; i < value.Length; i++)
            {
                char ch = value[i];
                switch (ch)
                {
                    case '\\': escaped.Append("\\\\"); break;
                    case '\"': escaped.Append("\\\""); break;
                    case '\n': escaped.Append("\\n"); break;
                    case '\r': escaped.Append("\\r"); break;
                    case '\t': escaped.Append("\\t"); break;
                    case '\b': escaped.Append("\\b"); break;
                    case '\f': escaped.Append("\\f"); break;
                    default:
                        if (ch < 32)
                            escaped.Append("\\u").Append(((int)ch).ToString("x4"));
                        else
                            escaped.Append(ch);
                        break;
                }
            }

            return escaped.ToString();
        }

        private static bool TryExtractReplyFallback(string raw, out string reply)
        {
            reply = null;
            if (string.IsNullOrWhiteSpace(raw)) return false;

            int choicesStart = raw.IndexOf("\"choices\"", StringComparison.Ordinal);
            if (choicesStart >= 0)
            {
                string choicesSlice = raw.Substring(choicesStart);

                if (TryExtractAnyJsonPropertyString(choicesSlice, "content", out reply) &&
                    !string.IsNullOrWhiteSpace(reply))
                    return true;

                if (TryExtractAnyJsonPropertyString(choicesSlice, "text", out reply) &&
                    !string.IsNullOrWhiteSpace(reply))
                    return true;
            }

            if (TryExtractAnyJsonPropertyString(raw, "response", out reply) &&
                !string.IsNullOrWhiteSpace(reply))
                return true;

            return false;
        }

        private static string SanitizeModelText(string value)
        {
            if (string.IsNullOrWhiteSpace(value)) return null;

            string normalized = value.Replace("\r\n", "\n").Replace('\r', '\n');
            var builder = new StringBuilder(normalized.Length);

            for (int i = 0; i < normalized.Length; i++)
            {
                char ch = normalized[i];
                if (ch == '^' && i + 1 < normalized.Length)
                {
                    char next = normalized[i + 1];
                    if (next == 'N' || next == 'M')
                    {
                        i++;
                        continue;
                    }
                }

                if (char.IsControl(ch) && ch != '\n' && ch != '\t')
                    continue;

                builder.Append(ch);
            }

            string cleaned = builder.ToString();
            while (cleaned.Contains("\n\n\n"))
                cleaned = cleaned.Replace("\n\n\n", "\n\n");

            return cleaned.Trim();
        }

        private static bool TryExtractReasoningContent(string raw, out string reasoning)
        {
            return TryExtractAnyJsonPropertyString(raw, "reasoning_content", out reasoning)
                && !string.IsNullOrWhiteSpace(reasoning);
        }

        private static bool TryDeriveReplyFromReasoning(string raw, out string reply)
        {
            reply = null;
            if (!TryExtractReasoningContent(raw, out string reasoning))
                return false;

            string cleaned = CollapseWhitespace(SanitizeModelText(reasoning));
            if (string.IsNullOrWhiteSpace(cleaned))
                return false;

            // Common cues seen in local reasoning traces.
            string[] markers =
            {
                "Final Output:",
                "Final output:",
                "Final Answer:",
                "Final answer:",
                "Answer:",
                "Output:",
                "Let's go with",
                "I will produce",
                "I'll produce",
                "I’ll produce",
            };

            for (int i = 0; i < markers.Length; i++)
            {
                if (TryExtractTailAfterMarker(cleaned, markers[i], out string candidate) &&
                    TryNormalizeDerivedCandidate(candidate, out reply))
                    return true;
            }

            if (TryExtractQuotedCandidate(cleaned, out string quoted) &&
                TryNormalizeDerivedCandidate(quoted, out reply))
                return true;

            if (TryExtractLastUsefulLine(cleaned, out string line) &&
                TryNormalizeDerivedCandidate(line, out reply))
                return true;

            return false;
        }

        private static bool TryExtractTailAfterMarker(string text, string marker, out string tail)
        {
            tail = null;
            if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(marker))
                return false;

            int idx = text.LastIndexOf(marker, StringComparison.OrdinalIgnoreCase);
            if (idx < 0) return false;

            int start = idx + marker.Length;
            if (start >= text.Length) return false;

            int end = text.IndexOf('\n', start);
            if (end < 0) end = text.Length;

            tail = text.Substring(start, end - start).Trim();
            return !string.IsNullOrWhiteSpace(tail);
        }

        private static bool TryExtractQuotedCandidate(string text, out string candidate)
        {
            candidate = null;
            if (string.IsNullOrWhiteSpace(text)) return false;

            int end = text.LastIndexOf('\"');
            while (end > 0)
            {
                int start = text.LastIndexOf('\"', end - 1);
                if (start < 0) break;

                string inside = text.Substring(start + 1, end - start - 1).Trim();
                if (!string.IsNullOrWhiteSpace(inside) && inside.Length <= 220)
                {
                    candidate = inside;
                    return true;
                }

                end = start - 1;
            }

            return false;
        }

        private static bool TryExtractLastUsefulLine(string text, out string candidate)
        {
            candidate = null;
            if (string.IsNullOrWhiteSpace(text)) return false;

            string[] lines = text.Split('\n');
            for (int i = lines.Length - 1; i >= 0; i--)
            {
                string line = lines[i].Trim();
                if (string.IsNullOrWhiteSpace(line)) continue;

                string lower = line.ToLowerInvariant();
                if (lower.StartsWith("thinking process")) continue;
                if (lower.StartsWith("analyze")) continue;
                if (lower.StartsWith("word count")) continue;
                if (lower.StartsWith("step")) continue;
                if (line.StartsWith("-")) continue;
                if (line.StartsWith("*")) continue;
                if (char.IsDigit(line[0])) continue;

                candidate = line;
                return true;
            }

            return false;
        }

        private static bool TryNormalizeDerivedCandidate(string raw, out string normalized)
        {
            normalized = null;
            if (string.IsNullOrWhiteSpace(raw)) return false;

            string text = CollapseWhitespace(SanitizeModelText(raw));
            if (string.IsNullOrWhiteSpace(text)) return false;

            while (text.StartsWith("\"", StringComparison.Ordinal) ||
                   text.StartsWith("'", StringComparison.Ordinal))
            {
                text = text.Substring(1).TrimStart();
            }

            while (text.EndsWith("\"", StringComparison.Ordinal) ||
                   text.EndsWith("'", StringComparison.Ordinal))
            {
                text = text.Substring(0, text.Length - 1).TrimEnd();
            }

            text = text.Replace("Answer:", string.Empty).Replace("Output:", string.Empty).Trim();
            if (text.Length < 2) return false;

            normalized = text;
            return true;
        }

        private static bool TryExtractAnyJsonPropertyString(string raw, string propertyName, out string value)
        {
            value = null;
            if (string.IsNullOrWhiteSpace(raw) || string.IsNullOrEmpty(propertyName))
                return false;

            string marker = "\"" + propertyName + "\"";
            int searchStart = 0;

            while (searchStart < raw.Length)
            {
                bool parsed = TryExtractJsonPropertyString(
                    raw,
                    propertyName,
                    searchStart,
                    out string candidate,
                    out int nextStart);

                if (nextStart <= searchStart)
                    break;
                searchStart = nextStart;

                if (!parsed)
                    continue;

                if (!string.IsNullOrWhiteSpace(candidate))
                {
                    value = candidate;
                    return true;
                }
            }

            return false;
        }

        private static bool TryExtractJsonPropertyString(string raw, string propertyName, out string value)
        {
            return TryExtractJsonPropertyString(raw, propertyName, 0, out value, out _);
        }

        private static bool TryExtractJsonPropertyString(
            string raw,
            string propertyName,
            int searchStart,
            out string value,
            out int nextSearchStart)
        {
            value = null;
            nextSearchStart = searchStart + 1;

            if (string.IsNullOrWhiteSpace(raw) || string.IsNullOrEmpty(propertyName))
                return false;

            string marker = "\"" + propertyName + "\"";
            int start = raw.IndexOf(marker, searchStart, StringComparison.Ordinal);
            if (start < 0)
            {
                nextSearchStart = raw.Length;
                return false;
            }

            nextSearchStart = start + marker.Length;
            int cursor = nextSearchStart;
            while (cursor < raw.Length && char.IsWhiteSpace(raw[cursor]))
                cursor++;

            if (cursor >= raw.Length || raw[cursor] != ':')
                return false;

            cursor++;
            while (cursor < raw.Length && char.IsWhiteSpace(raw[cursor]))
                cursor++;

            if (cursor >= raw.Length || raw[cursor] != '\"')
                return false;

            return TryReadJsonQuotedString(raw, cursor + 1, out value);
        }

        private static string CollapseWhitespace(string input)
        {
            if (string.IsNullOrWhiteSpace(input)) return string.Empty;

            var sb = new StringBuilder(input.Length);
            bool prevWhitespace = false;

            for (int i = 0; i < input.Length; i++)
            {
                char ch = input[i];
                bool ws = char.IsWhiteSpace(ch);

                if (ws)
                {
                    if (prevWhitespace) continue;
                    sb.Append(' ');
                    prevWhitespace = true;
                    continue;
                }

                sb.Append(ch);
                prevWhitespace = false;
            }

            return sb.ToString().Trim();
        }

        private static string TakeFirstSentences(string input, int maxSentences)
        {
            if (string.IsNullOrWhiteSpace(input) || maxSentences <= 0)
                return string.Empty;

            int sentenceCount = 0;
            int cutIndex = input.Length;

            for (int i = 0; i < input.Length; i++)
            {
                char ch = input[i];
                if (ch != '.' && ch != '!' && ch != '?') continue;

                sentenceCount++;
                if (sentenceCount >= maxSentences)
                {
                    cutIndex = i + 1;
                    break;
                }
            }

            return input.Substring(0, cutIndex).Trim();
        }

        private static string TrimToWordBoundary(string input, int maxChars)
        {
            if (string.IsNullOrWhiteSpace(input) || input.Length <= maxChars)
                return input;

            int end = maxChars;
            while (end > 0 && !char.IsWhiteSpace(input[end - 1]))
                end--;

            if (end < 12)
                end = maxChars;

            string trimmed = input.Substring(0, end).TrimEnd();
            if (trimmed.EndsWith(".") || trimmed.EndsWith("!") || trimmed.EndsWith("?"))
                return trimmed;

            return trimmed + "...";
        }

        private static bool TryReadJsonQuotedString(string raw, int startIndex, out string value)
        {
            value = null;
            if (string.IsNullOrEmpty(raw) || startIndex < 0 || startIndex > raw.Length)
                return false;

            var builder = new StringBuilder();
            bool escaped = false;

            for (int i = startIndex; i < raw.Length; i++)
            {
                char ch = raw[i];
                if (escaped)
                {
                    switch (ch)
                    {
                        case '\\':
                        case '\"':
                        case '/':
                            builder.Append(ch);
                            break;
                        case 'n':
                            builder.Append('\n');
                            break;
                        case 'r':
                            builder.Append('\r');
                            break;
                        case 't':
                            builder.Append('\t');
                            break;
                        case 'b':
                            builder.Append('\b');
                            break;
                        case 'f':
                            builder.Append('\f');
                            break;
                        case 'u':
                            if (i + 4 < raw.Length)
                            {
                                string hex = raw.Substring(i + 1, 4);
                                if (ushort.TryParse(hex, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out ushort codepoint))
                                {
                                    builder.Append((char)codepoint);
                                    i += 4;
                                    break;
                                }
                            }

                            builder.Append('u');
                            break;
                        default:
                            builder.Append(ch);
                            break;
                    }

                    escaped = false;
                    continue;
                }

                if (ch == '\\')
                {
                    escaped = true;
                    continue;
                }

                if (ch == '\"')
                {
                    value = builder.ToString();
                    return true;
                }

                builder.Append(ch);
            }

            return false;
        }

        private static string TryExtractInlineString(string raw, string marker)
        {
            TryExtractInlineString(raw, marker, out string value);
            return value;
        }

        private static bool TryExtractInlineString(string raw, string marker, out string value)
        {
            value = null;
            int start = raw.IndexOf(marker, StringComparison.Ordinal);
            if (start < 0) return false;

            start += marker.Length;
            var builder = new StringBuilder();
            bool escaped = false;

            for (int i = start; i < raw.Length; i++)
            {
                char ch = raw[i];
                if (escaped)
                {
                    switch (ch)
                    {
                        case '\\':
                        case '\"':
                        case '/':
                            builder.Append(ch);
                            break;
                        case 'n':
                            builder.Append('\n');
                            break;
                        case 'r':
                            builder.Append('\r');
                            break;
                        case 't':
                            builder.Append('\t');
                            break;
                        case 'b':
                            builder.Append('\b');
                            break;
                        case 'f':
                            builder.Append('\f');
                            break;
                        case 'u':
                            if (i + 4 < raw.Length)
                            {
                                string hex = raw.Substring(i + 1, 4);
                                if (ushort.TryParse(hex, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out ushort codepoint))
                                {
                                    builder.Append((char)codepoint);
                                    i += 4;
                                    break;
                                }
                            }

                            builder.Append('u');
                            break;
                        default:
                            builder.Append(ch);
                            break;
                    }

                    escaped = false;
                    continue;
                }

                if (ch == '\\')
                {
                    escaped = true;
                    continue;
                }

                if (ch == '\"')
                {
                    value = builder.ToString();
                    return true;
                }

                builder.Append(ch);
            }

            return false;
        }
    }
}
