import { SUCCESS_CODE, type ApiEnvelope } from '../types/api'
import { storage } from '../lib/storage'

export class ApiError extends Error {
  code: number
  constructor(code: number, message: string) {
    super(message)
    this.code = code
  }
}

type RequestOptions = {
  method?: string
  body?: unknown
  auth?: boolean
  memberAuth?: boolean
  providerToken?: string
  query?: Record<string, string | number | boolean | undefined>
}

let refreshPromise: Promise<void> | null = null

async function refreshTokens() {
  const refreshToken = storage.getRefreshToken()
  if (!refreshToken) throw new ApiError(0, '缺少 refresh token')
  const res = await fetch('/api/v1/auth/refresh', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ refresh_token: refreshToken }),
  })
  const json = (await res.json()) as ApiEnvelope<{
    access_token: string
    refresh_token: string
    uid: string
  }>
  if (json.code !== SUCCESS_CODE || !json.data) {
    storage.clearSession()
    throw new ApiError(json.code, json.message || 'refresh failed')
  }
  storage.setAccessToken(json.data.access_token)
  storage.setRefreshToken(json.data.refresh_token)
  storage.setUid(json.data.uid)
}

async function parseEnvelope<T>(res: Response): Promise<ApiEnvelope<T>> {
  const text = await res.text()
  if (!text.trim()) {
    if (!res.ok) {
      const hint =
        res.status === 404
          ? '後端找不到此 API，請確認已執行 make gen-api 並重啟 API（make restart-all）'
          : res.status === 405
            ? '後端尚未支援此操作，請重啟 API 服務（make run 或 scripts/restart-all.sh）'
            : res.status >= 500
            ? '後端 API 無法連線，請確認服務已啟動（scripts/start-all.sh 或 make -C haixun-backend start-all）'
            : `HTTP ${res.status}`
      throw new ApiError(res.status, hint)
    }
    return { code: SUCCESS_CODE, message: 'success', data: null as T }
  }
  try {
    return JSON.parse(text) as ApiEnvelope<T>
  } catch {
    throw new ApiError(res.status || 0, text.slice(0, 200) || 'invalid response')
  }
}

function buildURL(path: string, query?: RequestOptions['query']) {
  const url = new URL(path, window.location.origin)
  if (query) {
    for (const [k, v] of Object.entries(query)) {
      if (v !== undefined && v !== '') url.searchParams.set(k, String(v))
    }
  }
  return url.pathname + url.search
}

async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
  const buildHeaders = () => {
    const headers: Record<string, string> = { 'Content-Type': 'application/json' }
    if (opts.auth) {
      const token = storage.getAccessToken()
      if (token) headers.Authorization = `Bearer ${token}`
    }
    if (opts.memberAuth) {
      const token = storage.getAccessToken()
      if (token) headers['X-Member-Authorization'] = `Bearer ${token}`
    }
    if (opts.providerToken) {
      headers.Authorization = `Bearer ${opts.providerToken}`
    }
    return headers
  }

  const doFetch = () =>
    fetch(buildURL(path, opts.query), {
      method: opts.method ?? (opts.body ? 'POST' : 'GET'),
      headers: buildHeaders(),
      body: opts.body ? JSON.stringify(opts.body) : undefined,
    })

  let res = await doFetch()
  let json = await parseEnvelope<T>(res)

  if (json.code !== SUCCESS_CODE && res.status === 401 && opts.auth && storage.getRefreshToken()) {
    if (!refreshPromise) {
      refreshPromise = refreshTokens().finally(() => {
        refreshPromise = null
      })
    }
    try {
      await refreshPromise
      res = await doFetch()
      json = await parseEnvelope<T>(res)
    } catch {
      /* fall through */
    }
  }

  if (json.code !== SUCCESS_CODE) {
    throw new ApiError(json.code, json.message || 'request failed')
  }
  return json.data as T
}

export const api = {
  get: <T>(path: string, opts?: Omit<RequestOptions, 'method' | 'body'>) =>
    request<T>(path, { ...opts, method: 'GET' }),
  post: <T>(path: string, body?: unknown, opts?: Omit<RequestOptions, 'method' | 'body'>) =>
    request<T>(path, { ...opts, method: 'POST', body }),
  put: <T>(path: string, body?: unknown, opts?: Omit<RequestOptions, 'method' | 'body'>) =>
    request<T>(path, { ...opts, method: 'PUT', body }),
  patch: <T>(path: string, body?: unknown, opts?: Omit<RequestOptions, 'method' | 'body'>) =>
    request<T>(path, { ...opts, method: 'PATCH', body }),
  delete: <T>(path: string, opts?: Omit<RequestOptions, 'method' | 'body'>) =>
    request<T>(path, { ...opts, method: 'DELETE' }),
}

async function readStreamErrorMessage(res: Response): Promise<string> {
  const text = await res.text()
  if (!text.trim()) {
    if (res.status === 404) {
      return '後端尚未載入島民 API，請重啟 API（make restart-all 或 scripts/restart-all.sh）'
    }
    return `HTTP ${res.status}`
  }
  try {
    const json = JSON.parse(text) as { message?: string; code?: number }
    if (json.message) return json.message
  } catch {
    return text.slice(0, 200) || `HTTP ${res.status}`
  }
  return `HTTP ${res.status}`
}

async function consumeAIEventStream(
  res: Response,
  onDelta: (text: string) => void,
  onDone: (finishReason?: string) => void,
  onError: (msg: string) => void,
) {
  if (!res.ok || !res.body) {
    onError(await readStreamErrorMessage(res))
    return
  }

  const reader = res.body.getReader()
  const decoder = new TextDecoder()
  let buffer = ''

  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    buffer += decoder.decode(value, { stream: true })
    const parts = buffer.split('\n\n')
    buffer = parts.pop() ?? ''
    for (const part of parts) {
      const lines = part.split('\n')
      let event = ''
      let data = ''
      for (const line of lines) {
        if (line.startsWith('event:')) event = line.slice(6).trim()
        if (line.startsWith('data:')) data = line.slice(5).trim()
      }
      if (!data) continue
      try {
        const parsed = JSON.parse(data) as {
          type?: string
          text?: string
          finish_reason?: string
          message?: string
        }
        if (event === 'error' || parsed.type === 'error') {
          onError(parsed.message || 'stream error')
          return
        }
        if (parsed.type === 'delta' && parsed.text) onDelta(parsed.text)
        if (parsed.type === 'done') onDone(parsed.finish_reason)
      } catch {
        /* ignore malformed chunk */
      }
    }
  }
}

export async function streamIslanderChat(
  body: { messages: { role: string; content: string }[]; context: string },
  onDelta: (text: string) => void,
  onDone: (finishReason?: string) => void,
  onError: (msg: string) => void,
) {
  const memberToken = storage.getAccessToken()
  const headers: Record<string, string> = { 'Content-Type': 'application/json' }
  if (memberToken) headers.Authorization = `Bearer ${memberToken}`

  const res = await fetch('/api/v1/ai/islander/chat/stream', {
    method: 'POST',
    headers,
    body: JSON.stringify(body),
  })
  await consumeAIEventStream(res, onDelta, onDone, onError)
}

