import OpenAI from 'openai'
import { z } from 'zod'

import { decodeHtmlEntities } from '../lib/decode-html-entities'

const MODEL = 'gpt-5.2-2025-12-11'

const DEFAULT_SYSTEM_INSTRUCTIONS = [
	'Find all logical inconsistencies and contradictions in the following LLM prompt.',
	'As a result, produce a JSON object with a single field `issues` which is an array of objects of the shape:',
	'{"reason": string, "contradictingQuotes": string[], "type": "contradiction"}',
	'',
	'Rules:',
	'- Output must be valid JSON that matches the schema exactly (no markdown).',
	'- Each entry must include direct quotes copied verbatim from the prompt (no paraphrases).',
	'- If there are no issues, return {"issues":[]}.'
].join('\n')

const PromptIssueSchema = z.object({
	reason: z.string(),
	contradictingQuotes: z.array(z.string()),
	type: z.literal('contradiction')
})

const PromptIssuesEnvelopeSchema = z.object({
	issues: z.array(PromptIssueSchema)
})

export type PromptIssue = z.infer<typeof PromptIssueSchema>

export interface FindPromptIssuesResult {
	prompt: string
	issues: PromptIssue[]
	raw: Record<string, unknown>
}

export async function findPromptIssues(input: {
	agent: string
	step: string
	prompt: string
}): Promise<FindPromptIssuesResult> {
	const apiKey = process.env.OPENAI_API_KEY
	if (!apiKey) {
		throw new Error('OPENAI_API_KEY is not set')
	}

	const prompt = decodeHtmlEntities(String(input.prompt ?? ''))
	const instructions = envTextOrDefault(
		'BOSUN_PROMPT_ISSUES_INSTRUCTIONS',
		DEFAULT_SYSTEM_INSTRUCTIONS
	)

	const client = new OpenAI({ apiKey })
	const response = await client.responses.create({
		model: MODEL,
		instructions,
		input: prompt,
		reasoning: { effort: 'low' },
		text: {
			format: {
				type: 'json_schema',
				name: 'prompt_issues',
				strict: true,
				schema: {
					type: 'object',
					additionalProperties: false,
					required: ['issues'],
					properties: {
						issues: {
							type: 'array',
							items: {
								type: 'object',
								additionalProperties: false,
								required: ['reason', 'contradictingQuotes', 'type'],
								properties: {
									reason: { type: 'string' },
									contradictingQuotes: { type: 'array', items: { type: 'string' } },
									type: { type: 'string', enum: ['contradiction'] }
								}
							}
						}
					}
				}
			}
		}
	})

	const issues = parseIssues(response.output_text)
	const normalized = normalizeIssues(issues)

	return {
		prompt,
		issues: normalized,
		raw: {
			input: {
				agent: input.agent,
				step: input.step,
				promptChars: prompt.length
			},
			instructions: {
				env: 'BOSUN_PROMPT_ISSUES_INSTRUCTIONS',
				overridden: instructions !== DEFAULT_SYSTEM_INSTRUCTIONS,
				chars: instructions.length
			},
			model: MODEL,
			reasoning: { effort: 'low' },
			response: {
				id: response.id,
				status: response.status,
				outputTextChars: response.output_text.length
			},
			result: {
				issues: normalized.length
			}
		}
	}
}

function parseIssues(raw: string): PromptIssue[] {
	const text = stripCodeFences(String(raw || '')).trim()
	const parsed = tryParseJson(text) ?? tryParseJson(extractJsonArray(text))
	if (!parsed) throw new Error('Failed to parse issues response as JSON')
	if (Array.isArray(parsed)) {
		return PromptIssuesEnvelopeSchema.parse({ issues: parsed }).issues
	}
	return PromptIssuesEnvelopeSchema.parse(parsed).issues
}

function normalizeIssues(issues: PromptIssue[]): PromptIssue[] {
	return (issues || [])
		.map((issue) => {
			const reason = String(issue.reason || '').trim()
			const quotes = [
				...new Set(
					(issue.contradictingQuotes || [])
						.map((q) => normalizeQuote(String(q)))
						.filter((q) => q.length > 0)
				)
			]
			return { type: 'contradiction' as const, reason, contradictingQuotes: quotes }
		})
		.filter((i) => i.reason.length > 0 || i.contradictingQuotes.length > 0)
}

function normalizeQuote(raw: string): string {
	let q = decodeHtmlEntities(raw).trim()
	q = trimWrappingQuotes(q)
	return q.trim()
}

function trimWrappingQuotes(text: string): string {
	const quotePairs: Array<[string, string]> = [
		['"', '"'],
		["'", "'"],
		['“', '”'],
		['‘', '’'],
		['«', '»'],
		['„', '“']
	]

	let out = text
	for (let i = 0; i < 5; i++) {
		const trimmed = out.trim()
		let changed = false
		for (const [open, close] of quotePairs) {
			if (trimmed.startsWith(open) && trimmed.endsWith(close)) {
				const next = trimmed.slice(open.length, trimmed.length - close.length)
				if (next === trimmed) continue
				out = next
				changed = true
				break
			}
		}
		if (!changed) {
			out = trimmed
			break
		}
	}
	return out
}

function stripCodeFences(text: string): string {
	const trimmed = text.trim()
	const m = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)
	return m ? m[1] : trimmed
}

function tryParseJson(text: string): unknown | null {
	try {
		return JSON.parse(text)
	} catch {
		return null
	}
}

function extractJsonArray(text: string): string {
	const start = text.indexOf('[')
	const end = text.lastIndexOf(']')
	if (start === -1 || end === -1 || end < start) return text
	return text.slice(start, end + 1)
}

function envTextOrDefault(key: string, defaultValue: string): string {
	const raw = (process.env[key] ?? '').trim()
	if (!raw) return defaultValue
	return raw.replace(/\\n/g, '\n')
}
