import {z} from 'zod'

import {parseBbox, type Bbox} from '../geo'
import {CrumbPlaceSchema, PlaceRefSchema} from './places'

export const CRUMB_NAME_MAX = 200
export const CRUMB_NOTE_MAX = 2000

export const TagNameSchema = z
  .string()
  .trim()
  .toLowerCase()
  .min(1)
  .max(32)
  .regex(
    /^[a-z0-9][a-z0-9-]*$/,
    'tag must be lowercase alphanumeric or dashes',
  )

const slugifyTag = (raw: string): string =>
  raw
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 32)

export const normalizeTags = (raw: string[], max = 5): string[] => {
  const seen = new Set<string>()
  for (const tag of raw) {
    const slug = slugifyTag(tag)
    if (slug) seen.add(slug)
    if (seen.size >= max) break
  }
  return [...seen]
}

export const CrumbSourceSchema = z.enum([
  'manual',
  'screengrab',
  'link',
])

export const CrumbStatusSchema = z.enum(['saved', 'planned', 'done'])

export const SourceKindSchema = z.enum(['screengrab', 'link'])

export const EventStatusSchema = z.enum([
  'scheduled',
  'cancelled',
  'postponed',
])

export const ReminderKindSchema = z.enum([
  'manual',
  'week_before',
  'day_before',
])

export const ReminderSchema = z.object({
  id: z.uuid(),
  crumbId: z.uuid(),
  remindAt: z.iso.datetime(),
  kind: ReminderKindSchema,
})

export const CreateReminderSchema = z.object({
  remindAt: z.iso.datetime(),
})

export const RegisterPushTokenSchema = z.object({
  token: z.string().min(1).max(200),
  platform: z.enum(['ios']).default('ios'),
  timezone: z.string().max(64).optional(),
})

export const CrumbEventSchema = z.object({
  id: z.uuid(),
  name: z.string(),
  startsAt: z.iso.datetime().nullable(),
  endsAt: z.iso.datetime().nullable(),
  url: z.string().nullable(),
  status: EventStatusSchema,
})

export const CrumbOriginSchema = z.object({
  id: z.uuid(),
  kind: SourceKindSchema,
  url: z.string().nullable(),
})

// free-form location details that become a provisional place
export const PlaceDraftSchema = z
  .object({
    name: z.string().trim().min(1).max(CRUMB_NAME_MAX),
    address: z.string().max(500).optional(),
    latitude: z.number().min(-90).max(90).nullish(),
    longitude: z.number().min(-180).max(180).nullish(),
  })
  .refine((v) => (v.latitude == null) === (v.longitude == null), {
    message: 'latitude and longitude must be provided together',
    path: ['latitude'],
  })

const crumbTargetFields = {
  placeId: z.uuid().optional(),
  placeRef: PlaceRefSchema.optional(),
  placeDraft: PlaceDraftSchema.optional(),
  eventId: z.uuid().optional(),
}

const atMostOneTarget = (v: {
  placeId?: unknown
  placeRef?: unknown
  placeDraft?: unknown
  eventId?: unknown
}) =>
  [v.placeId, v.placeRef, v.placeDraft, v.eventId].filter(
    (t) => t != null,
  ).length <= 1

export const CreateCrumbSchema = z
  .object({
    // client-minted id for an idempotent create: a replayed create whose ack
    // was lost to a dropped connection upserts on this pk instead of doubling
    id: z.uuid().optional(),
    name: z.string().trim().min(1).max(CRUMB_NAME_MAX),
    note: z.string().trim().max(CRUMB_NOTE_MAX).optional(),
    tags: z.array(TagNameSchema).max(16).optional(),
    source: CrumbSourceSchema.default('manual'),
    sourceId: z.uuid().optional(),
    ...crumbTargetFields,
  })
  .refine(atMostOneTarget, {
    message: 'crumb can target at most one place or event',
    path: ['placeId'],
  })

export const UpdateCrumbSchema = z
  .object({
    name: z.string().trim().min(1).max(CRUMB_NAME_MAX).optional(),
    note: z.string().trim().max(CRUMB_NOTE_MAX).nullish(),
    tags: z.array(TagNameSchema).max(16).optional(),
    status: CrumbStatusSchema.optional(),
    plannedFor: z.iso.datetime().nullish(),
    ...crumbTargetFields,
  })
  .refine(atMostOneTarget, {
    message: 'crumb can target at most one place or event',
    path: ['placeId'],
  })

export const CrumbSchema = z.object({
  id: z.uuid(),
  authorId: z.uuid(),
  name: z.string(),
  note: z.string().nullable(),
  imageUrl: z.string().nullable(),
  source: CrumbSourceSchema,
  status: CrumbStatusSchema,
  plannedFor: z.iso.datetime().nullable(),
  place: CrumbPlaceSchema.nullable(),
  event: CrumbEventSchema.nullable(),
  origin: CrumbOriginSchema.nullable(),
  tags: z.array(z.object({name: z.string(), emoji: z.string()})),
  createdAt: z.iso.datetime(),
  updatedAt: z.iso.datetime(),
})

export const ExtractCrumbDraftSchema = z.object({
  name: z.string(),
  note: z.string().nullable(),
  tags: z.array(z.string()),
  source: SourceKindSchema,
  sourceId: z.uuid().nullable(),
  imageUrl: z.string().nullable(),
  placeId: z.uuid().nullable(),
  place: CrumbPlaceSchema.nullable(),
  placeDraft: PlaceDraftSchema.nullable(),
  eventId: z.uuid().nullable(),
  event: CrumbEventSchema.nullable(),
  resolved: z.boolean(),
})

export const ExtractCrumbResponseSchema = z.object({
  drafts: z.array(ExtractCrumbDraftSchema),
})

export const CrumbResponseSchema = z.object({crumb: CrumbSchema})

export const TagSuggestionsResponseSchema = z.object({
  fromPlace: z.array(z.string()),
  recent: z.array(z.string()),
})

export const TagCountSchema = z.object({
  name: z.string(),
  emoji: z.string(),
  count: z.number().int().nonnegative(),
})

// the map reads its own crumbs aggregated to one point per place; the same
// all | tag | status filter as the map screen, never both tag and status
export const MapBoundsQuerySchema = z
  .object({
    bbox: z.string().transform((raw, ctx): Bbox => {
      const parsed = parseBbox(raw)
      if (!parsed) {
        ctx.addIssue({code: 'custom', message: 'invalid bbox'})
        return z.NEVER
      }
      return parsed
    }),
    zoom: z.coerce.number().int().min(0).max(22).optional(),
    tag: TagNameSchema.optional(),
    status: z.enum(['planned', 'done']).optional(),
  })
  .refine((v) => !(v.tag && v.status), {
    message: 'tag and status are mutually exclusive',
    path: ['tag'],
  })

export const MapPlacePointSchema = z.object({
  placeId: z.uuid(),
  name: z.string(),
  latitude: z.number(),
  longitude: z.number(),
  category: z.string().nullable(),
  crumbCount: z.number().int().nonnegative(),
  // representative crumb; lets a single-crumb pin open the crumb directly
  crumbId: z.uuid(),
})

export type CreateCrumbInput = z.infer<typeof CreateCrumbSchema>
export type UpdateCrumbInput = z.infer<typeof UpdateCrumbSchema>
export type Crumb = z.infer<typeof CrumbSchema>
export type CrumbSource = z.infer<typeof CrumbSourceSchema>
export type CrumbStatus = z.infer<typeof CrumbStatusSchema>
export type SourceKind = z.infer<typeof SourceKindSchema>
export type EventStatus = z.infer<typeof EventStatusSchema>
export type CrumbEvent = z.infer<typeof CrumbEventSchema>
export type CrumbOrigin = z.infer<typeof CrumbOriginSchema>
export type ReminderKind = z.infer<typeof ReminderKindSchema>
export type Reminder = z.infer<typeof ReminderSchema>
export type PlaceDraft = z.infer<typeof PlaceDraftSchema>
export type ExtractCrumbDraft = z.infer<
  typeof ExtractCrumbDraftSchema
>
export type TagSuggestions = z.infer<
  typeof TagSuggestionsResponseSchema
>
export type TagCount = z.infer<typeof TagCountSchema>
export type MapBoundsQuery = z.infer<typeof MapBoundsQuerySchema>
export type MapPlacePoint = z.infer<typeof MapPlacePointSchema>
