import Anthropic from '@anthropic-ai/sdk'
import {HTTPException} from 'hono/http-exception'
import {z} from 'zod'

import type {
  ExtractedEntity,
  VisionImage,
  VisionProvider,
} from './types'

const MODEL = 'claude-sonnet-4-6'
const MAX_TOKENS = 2048
const MAX_ENTITIES = 10

const SUPPORTED_MEDIA_TYPES = [
  'image/jpeg',
  'image/png',
  'image/gif',
  'image/webp',
] as const

const TOOL_NAME = 'record_crumbs'

const entitySchema = {
  type: 'object' as const,
  properties: {
    name: {
      type: 'string',
      description:
        'a place, business, event, product, or subject shown in the screengrab',
    },
    category: {
      type: ['string', 'null'],
      description:
        'a short category if evident (e.g. "coffee", "restaurant", "museum"), else null',
    },
    addressHint: {
      type: ['string', 'null'],
      description:
        'any locality text that helps locate the place (city, neighbourhood, street, full address), else null',
    },
    startsAt: {
      type: ['string', 'null'],
      description:
        'iso 8601 start datetime if the subject is a dated event and the date is visible, else null',
    },
    tags: {
      type: 'array',
      items: {type: 'string'},
      maxItems: 3,
      description:
        '1 to 3 broad, reusable tags — the general kind of thing it is or ' +
        'why someone would save it, e.g. "coffee", "restaurant", "ramen", ' +
        '"movie", "book", "recipe", "travel", "date-night", "buy". Prefer ' +
        'high-level categories and intents; avoid narrow descriptors like ' +
        '"single-origin" or "mid-century-modern". lowercase, one or two words.',
    },
  },
  required: ['name', 'category', 'addressHint', 'startsAt', 'tags'],
}

const toolSchema = {
  type: 'object' as const,
  properties: {
    entities: {
      type: 'array',
      items: entitySchema,
      description:
        'every distinct place or event worth saving, in display order',
    },
  },
  required: ['entities'],
}

const EntitySchema = z.object({
  name: z.string().min(1),
  category: z.string().nullable(),
  addressHint: z.string().nullable(),
  startsAt: z.string().nullable().catch(null),
  tags: z.array(z.string()),
})

const ResultSchema = z.object({
  entities: z.array(EntitySchema).min(1),
})

const PROMPT =
  'Extract every distinct place or event from this screengrab so each can be saved as a bookmark. ' +
  'Use the record_crumbs tool. A list (e.g. "5 bakeries to try") yields one entity per item; ' +
  'a single subject yields one entity. Prefer real-world place or business names. ' +
  'Keep tags high-level: a few broad categories or intents (like "coffee", "restaurant", ' +
  '"movie", "recipe", "travel"), the way someone would group saves — not specific descriptors. ' +
  'Do not invent details that are not visible.'

const toHttpException = (err: unknown): unknown => {
  if (err instanceof Anthropic.APIError) {
    if (err.status === 401)
      return new HTTPException(401, {message: 'invalid_llm_key'})
    if (err.status === 429)
      return new HTTPException(429, {message: 'llm_rate_limited'})
    if (err.status === 529)
      return new HTTPException(503, {message: 'llm_unavailable'})
    if (err.status && err.status >= 400 && err.status < 500) {
      console.error('llm request rejected', err.status, err.message)
      return new HTTPException(502, {message: 'upstream_error'})
    }
  }
  return err
}

const isSupportedMediaType = (
  m: string,
): m is (typeof SUPPORTED_MEDIA_TYPES)[number] =>
  (SUPPORTED_MEDIA_TYPES as readonly string[]).includes(m)

export const createClaudeProvider = (): VisionProvider => ({
  async extract(
    image: VisionImage,
    apiKey: string,
  ): Promise<ExtractedEntity[]> {
    if (!isSupportedMediaType(image.mimeType)) {
      console.error('unsupported image type', image.mimeType)
      throw new HTTPException(400, {
        message: 'unsupported_image_type',
      })
    }

    const client = new Anthropic({apiKey})

    const res = await client.messages
      .create({
        model: MODEL,
        max_tokens: MAX_TOKENS,
        tools: [
          {
            name: TOOL_NAME,
            description:
              'record the places or events found in the screengrab',
            input_schema: toolSchema,
          },
        ],
        tool_choice: {type: 'tool', name: TOOL_NAME},
        messages: [
          {
            role: 'user',
            content: [
              {
                type: 'image',
                source: {
                  type: 'base64',
                  media_type: image.mimeType,
                  data: image.base64,
                },
              },
              {type: 'text', text: PROMPT},
            ],
          },
        ],
      })
      .catch((err) => {
        throw toHttpException(err)
      })

    const toolUse = res.content.find((b) => b.type === 'tool_use')
    if (!toolUse || toolUse.type !== 'tool_use')
      throw new HTTPException(502, {
        message: 'llm_malformed_output',
      })

    const parsed = ResultSchema.safeParse(toolUse.input)
    if (!parsed.success)
      throw new HTTPException(502, {
        message: 'llm_malformed_output',
      })

    return parsed.data.entities.slice(0, MAX_ENTITIES)
  },
})
