export type OrgStatus = 'INFO_PENDING' | 'READY' | 'PARTIAL' | 'ACTIVE'

export type ContactCategory = 'APPLICANT' | 'MJ' | 'LAWYER' | 'OPERATOR'

/** 담당자관리에 등록된 담당자 1명. 신청기관 담당자는 affiliation에 소속 기관명이 들어간다. */
export interface Contact {
  id: number
  category: ContactCategory
  name: string
  affiliation: string | null
  deptName: string | null
  title: string | null
  phone: string | null
  email: string | null
}

/** 담당자 등록/수정 폼 입력. 폼 상태는 문자열로만 다루므로 빈 문자열이 "값 없음"을 뜻한다. */
export interface ContactInput {
  category: ContactCategory
  name: string
  affiliation: string
  deptName: string
  title: string
  phone: string
  email: string
}

export interface Org {
  id: number
  orgNo: string
  orgName: string
  status: OrgStatus
  applicant: Contact | null
  mj: Contact | null
  lawyer: Contact | null
  lawyerAssignedDate: string | null
  channelIdMj: string | null
  channelIdLaw: string | null
}

/** 채널관리 화면이 신청기관/문정원/변호사 배정을 한 번에 바꿀 때 보내는 요청. null은 해제다. */
export interface AssignmentInput {
  applicantContactId: number | null
  mjContactId: number | null
  lawyerContactId: number | null
  lawyerAssignedDate: string | null
}

export type ProvisionOutcome = 'CACHED' | 'RECOVERED' | 'CREATED' | 'FAILED'

export interface ProvisionResult {
  mj: ProvisionOutcome
  law: ProvisionOutcome
  message: string
}

export interface SeedReport {
  created: number
  updated: number
  total: number
}

export interface FileRef {
  id: string
  name: string
  size: number
}

export interface PostView {
  id: string
  user: string
  message: string
  createAt: number
  system: boolean
  files: FileRef[]
}

export interface FileView {
  id: string
  name: string
  size: number
  mimeType: string
  createAt: number
  uploader: string
}

export type ChannelKindKey = 'mj' | 'law'

export interface WorkMemo {
  id: number
  orgId: number
  contactName: string
  body: string
  createdAt: number
}

/**
 * 서버가 4xx/5xx로 응답했을 때 던지는 예외. status를 함께 들고 있어야 호출자가
 * "인증이 끊긴 것(401)"과 "그 밖의 모든 오류"를 구분해서 다르게 처리할 수 있다 -
 * 그렇지 않으면(예: 평범한 Error) 서버 오류나 네트워크 문제까지 전부 로그아웃으로
 * 오인해 사용자를 로그인 화면으로 쫓아내게 된다.
 */
export class ApiError extends Error {
  constructor(
    public status: number,
    message: string,
  ) {
    super(message)
    this.name = 'ApiError'
  }
}

/** Spring Security가 내려주는 CSRF 쿠키를 읽어 헤더로 되돌려 보낸다. */
function csrfToken(): string {
  const match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]*)/)
  return match ? decodeURIComponent(match[1]) : ''
}

async function request<T>(url: string, init: RequestInit = {}): Promise<T> {
  const method = init.method ?? 'GET'
  const headers: Record<string, string> = { ...(init.headers as Record<string, string>) }

  if (method !== 'GET') {
    headers['X-XSRF-TOKEN'] = csrfToken()
  }

  const response = await fetch(url, { ...init, method, headers, credentials: 'same-origin' })

  if (!response.ok) {
    const body = await response.text()
    throw new ApiError(response.status, `요청 실패 (${response.status}): ${body}`)
  }
  return response.json() as Promise<T>
}

export function getOrgs(): Promise<Org[]> {
  return request<Org[]>('/api/orgs')
}

export function updateAssignments(orgId: number, body: AssignmentInput): Promise<Org> {
  return request<Org>(`/api/orgs/${orgId}/assignments`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })
}

export function getContacts(category?: ContactCategory): Promise<Contact[]> {
  const query = category ? `?category=${category}` : ''
  return request<Contact[]>(`/api/contacts${query}`)
}

export function createContact(body: ContactInput): Promise<Contact> {
  return request<Contact>('/api/contacts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })
}

export function updateContactInfo(id: number, body: ContactInput): Promise<Contact> {
  return request<Contact>(`/api/contacts/${id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })
}

/** 204를 돌려주므로 request()의 json() 경로를 타지 않고 직접 부른다(deleteMemo()와 동일한 패턴). */
export async function deleteContact(id: number): Promise<void> {
  const response = await fetch(`/api/contacts/${id}`, {
    method: 'DELETE',
    credentials: 'same-origin',
    headers: { 'X-XSRF-TOKEN': csrfToken() },
  })
  if (!response.ok) {
    const body = await response.text()
    throw new ApiError(response.status, `요청 실패 (${response.status}): ${body}`)
  }
}

export function provisionChannels(id: number): Promise<ProvisionResult> {
  return request<ProvisionResult>(`/api/orgs/${id}/channels`, { method: 'POST' })
}

export function getPosts(orgId: number, channel: ChannelKindKey): Promise<PostView[]> {
  return request<PostView[]>(`/api/orgs/${orgId}/posts?channel=${channel}`)
}

export function getFiles(orgId: number, channel: ChannelKindKey): Promise<FileView[]> {
  return request<FileView[]>(`/api/orgs/${orgId}/files?channel=${channel}`)
}

/** 다운로드는 <a href>가 그대로 열게 두므로 fetch()를 거치지 않는다. */
export function fileDownloadUrl(fileId: string): string {
  return `/api/files/${fileId}`
}

export interface SendResult {
  post: PostView
  /** 게시글은 전송됐지만 공지 뒤처리(핀/헤더)가 일부 실패한 경우의 경고. 성공이면 null */
  noticeWarning: string | null
}

export function sendMessage(
  orgId: number,
  channel: ChannelKindKey,
  body: { message: string; notice: boolean; files: File[] },
): Promise<SendResult> {
  const form = new FormData()
  form.append('channel', channel)
  form.append('message', body.message)
  form.append('notice', String(body.notice))
  for (const file of body.files) {
    form.append('files', file)
  }
  return request<SendResult>(`/api/orgs/${orgId}/messages`, { method: 'POST', body: form })
}

/** 공지가 없으면 서버가 204를 주므로 request()의 json() 경로를 타지 않고 직접 부른다. */
export async function getNotice(orgId: number, channel: ChannelKindKey): Promise<PostView | null> {
  const response = await fetch(`/api/orgs/${orgId}/notice?channel=${channel}`, {
    credentials: 'same-origin',
  })
  if (response.status === 204) {
    return null
  }
  if (!response.ok) {
    throw new ApiError(response.status, `요청 실패 (${response.status})`)
  }
  return response.json() as Promise<PostView>
}

export async function clearNotice(orgId: number, channel: ChannelKindKey): Promise<void> {
  const response = await fetch(`/api/orgs/${orgId}/notice?channel=${channel}`, {
    method: 'DELETE',
    credentials: 'same-origin',
    headers: { 'X-XSRF-TOKEN': csrfToken() },
  })
  if (!response.ok) {
    throw new ApiError(response.status, `공지 해제 실패 (${response.status})`)
  }
}

export function getMemos(orgId: number): Promise<WorkMemo[]> {
  return request<WorkMemo[]>(`/api/orgs/${orgId}/memos`)
}

export function createMemo(
  orgId: number,
  memo: { contactName: string; body: string },
): Promise<WorkMemo> {
  return request<WorkMemo>(`/api/orgs/${orgId}/memos`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(memo),
  })
}

/** 204를 돌려주므로 request()의 json() 경로를 타지 않고 직접 부른다(logout()과 동일한 패턴). */
export async function deleteMemo(orgId: number, memoId: number): Promise<void> {
  const response = await fetch(`/api/orgs/${orgId}/memos/${memoId}`, {
    method: 'DELETE',
    credentials: 'same-origin',
    headers: { 'X-XSRF-TOKEN': csrfToken() },
  })
  if (!response.ok) {
    const body = await response.text()
    throw new ApiError(response.status, `요청 실패 (${response.status}): ${body}`)
  }
}

export function uploadSeed(file: File): Promise<SeedReport> {
  const form = new FormData()
  form.append('file', file)
  return request<SeedReport>('/api/seed', { method: 'POST', body: form })
}

export interface MemberImportReport {
  created: number
  updated: number
  skipped: number
  assigned: number
}

export function importMembers(file: File): Promise<MemberImportReport> {
  const form = new FormData()
  form.append('file', file)
  return request<MemberImportReport>('/api/contacts/import', { method: 'POST', body: form })
}

/** 204를 돌려주므로 request()의 json() 경로를 타지 않고 직접 부른다. */
export async function logout(): Promise<void> {
  const response = await fetch('/api/auth/logout', {
    method: 'POST',
    credentials: 'same-origin',
    headers: { 'X-XSRF-TOKEN': csrfToken() },
  })
  if (!response.ok) {
    throw new ApiError(response.status, `로그아웃 실패 (${response.status})`)
  }
}

export async function login(username: string, password: string): Promise<void> {
  const body = new URLSearchParams({ username, password })
  const response = await fetch('/api/auth/login', {
    method: 'POST',
    credentials: 'same-origin',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'X-XSRF-TOKEN': csrfToken(),
    },
    body,
  })
  if (!response.ok) {
    throw new ApiError(response.status, '아이디 또는 비밀번호가 올바르지 않습니다.')
  }
}
