export interface CurrentUser {
  id: string
  userName: string
  roles: string[]
  isActive: boolean
  mustChangePassword: boolean
}

export const ROLE_ADMIN = 'admin'

interface ApiMessageResponse {
  message?: unknown
  errors?: unknown
}

interface LoginPayload {
  userName: string
  password: string
}

interface ChangePasswordPayload {
  oldPassword: string
  newPassword: string
  newPasswordConfirm: string
}

interface ApiErrorPayload {
  message: string | null
  errors: string[]
}

export class AuthRequestError extends Error {
  status: number

  constructor(message: string, status: number) {
    super(message)
    this.name = 'AuthRequestError'
    this.status = status
  }
}

let cachedUser: CurrentUser | null = null
let sessionResolved = false
let pendingSessionRequest: Promise<CurrentUser | null> | null = null

function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null
}

function normalizeCurrentUser(value: unknown): CurrentUser | null {
  if (!isObject(value)) {
    return null
  }

  const { id, userName, roles, isActive, mustChangePassword } = value

  if (typeof id !== 'string') {
    return null
  }

  const normalizedUserName = typeof userName === 'string' ? userName : ''

  const normalizedRoles = Array.isArray(roles)
    ? roles.filter((role): role is string => typeof role === 'string').map((role) => role.trim()).filter((role) => role.length > 0)
    : []

  return {
    id,
    userName: normalizedUserName,
    roles: normalizedRoles,
    isActive: Boolean(isActive),
    mustChangePassword: Boolean(mustChangePassword),
  }
}

export function hasRole(user: CurrentUser | null | undefined, role: string): boolean {
  if (!user) {
    return false
  }

  const normalizedRole = role.trim().toLowerCase()
  if (normalizedRole.length === 0) {
    return false
  }

  return user.roles.some((userRole) => userRole.toLowerCase() === normalizedRole)
}

function isUnauthenticatedResponse(response: Response) {
  if (response.status === 401 || response.status === 403) {
    return true
  }

  if (!response.redirected) {
    return false
  }

  try {
    const url = new URL(response.url)
    return url.pathname === '/auth/login'
  } catch {
    return false
  }
}

async function readApiMessage(response: Response): Promise<string | null> {
  const payload = await readApiErrorPayload(response)
  return payload.message
}

function normalizeApiErrors(value: unknown): string[] {
  if (!Array.isArray(value)) {
    return []
  }

  return value
    .filter((entry): entry is string => typeof entry === 'string')
    .map((entry) => entry.trim())
    .filter((entry) => entry.length > 0)
}

async function readApiErrorPayload(response: Response): Promise<ApiErrorPayload> {
  const contentType = response.headers.get('content-type') ?? ''
  if (!contentType.toLowerCase().includes('application/json')) {
    return { message: null, errors: [] }
  }

  try {
    const body = (await response.json()) as ApiMessageResponse
    const message =
      typeof body.message === 'string' && body.message.trim().length > 0
        ? body.message.trim()
        : null

    return {
      message,
      errors: normalizeApiErrors(body.errors),
    }
  } catch {
    return { message: null, errors: [] }
  }
}

function toAuthError(error: unknown, fallbackMessage: string): AuthRequestError {
  if (error instanceof AuthRequestError) {
    return error
  }

  return new AuthRequestError(fallbackMessage, 0)
}

export function clearAuthSession() {
  cachedUser = null
  sessionResolved = false
  pendingSessionRequest = null
}

export async function fetchCurrentUser(options: { force?: boolean } = {}): Promise<CurrentUser | null> {
  const shouldForce = options.force === true

  if (!shouldForce && sessionResolved) {
    return cachedUser
  }

  if (!shouldForce && pendingSessionRequest) {
    return pendingSessionRequest
  }

  const request = (async () => {
    let response: Response

    try {
      response = await fetch('/auth/me', {
        method: 'GET',
        credentials: 'include',
        headers: {
          Accept: 'application/json',
        },
      })
    } catch (error) {
      throw toAuthError(error, 'Server ist nicht erreichbar. Bitte später erneut versuchen.')
    }

    if (isUnauthenticatedResponse(response)) {
      cachedUser = null
      sessionResolved = true
      return null
    }

    if (!response.ok) {
      const apiMessage = await readApiMessage(response)
      throw new AuthRequestError(
        apiMessage ?? 'Benutzerdaten konnten nicht geladen werden.',
        response.status,
      )
    }

    const rawPayload: unknown = await response.json()
    const user = normalizeCurrentUser(rawPayload)

    if (!user) {
      throw new AuthRequestError('Antwortformat von /auth/me ist ungültig.', response.status)
    }

    cachedUser = user
    sessionResolved = true
    return user
  })().finally(() => {
    pendingSessionRequest = null
  })

  pendingSessionRequest = request
  return request
}

export async function login(payload: LoginPayload): Promise<void> {
  let response: Response

  try {
    response = await fetch('/auth/login', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      body: JSON.stringify({
        userName: payload.userName,
        password: payload.password,
      }),
    })
  } catch (error) {
    throw toAuthError(error, 'Server ist nicht erreichbar. Bitte später erneut versuchen.')
  }

  if (!response.ok) {
    const apiMessage = await readApiMessage(response)

    if (response.status === 400) {
      throw new AuthRequestError(
        apiMessage ?? 'Bitte Benutzername und Passwort eingeben.',
        response.status,
      )
    }

    if (response.status === 401) {
      throw new AuthRequestError(apiMessage ?? 'Ungültige Anmeldedaten.', response.status)
    }

    if (response.status === 403) {
      throw new AuthRequestError('Dieses Konto ist nicht aktiv.', response.status)
    }

    throw new AuthRequestError(
      apiMessage ?? 'Anmeldung fehlgeschlagen. Bitte versuche es erneut.',
      response.status,
    )
  }

  sessionResolved = false
  await fetchCurrentUser({ force: true })
}

export async function logout(): Promise<void> {
  let response: Response

  try {
    response = await fetch('/auth/logout', {
      method: 'POST',
      credentials: 'include',
      headers: {
        Accept: 'application/json',
      },
    })
  } catch (error) {
    throw toAuthError(error, 'Server ist nicht erreichbar. Bitte später erneut versuchen.')
  }

  if (response.status === 401 || response.status === 403) {
    clearAuthSession()
    return
  }

  if (!response.ok) {
    const apiMessage = await readApiMessage(response)
    throw new AuthRequestError(
      apiMessage ?? 'Abmeldung fehlgeschlagen. Bitte versuche es erneut.',
      response.status,
    )
  }

  clearAuthSession()
}

export async function changePassword(payload: ChangePasswordPayload): Promise<string> {
  let response: Response

  try {
    response = await fetch('/auth/password', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      body: JSON.stringify(payload),
    })
  } catch (error) {
    throw toAuthError(error, 'Server ist nicht erreichbar. Bitte später erneut versuchen.')
  }

  if (response.status === 401 || response.status === 403) {
    clearAuthSession()
    throw new AuthRequestError('Session abgelaufen. Bitte melde dich erneut an.', response.status)
  }

  if (!response.ok) {
    const apiError = await readApiErrorPayload(response)

    if (response.status === 400) {
      const fallback = 'Passwortänderung fehlgeschlagen. Bitte Eingaben prüfen.'
      const details = apiError.errors.length > 0 ? ` ${apiError.errors.join(' ')}` : ''
      throw new AuthRequestError((apiError.message ?? fallback) + details, response.status)
    }

    throw new AuthRequestError(
      apiError.message ?? 'Passwortänderung fehlgeschlagen. Bitte versuche es erneut.',
      response.status,
    )
  }

  const successMessage = await readApiMessage(response)
  clearAuthSession()
  return successMessage ?? 'Passwort erfolgreich geändert.'
}
