import { Oauth2Driver } from '@adonisjs/ally'
import type { HttpContext } from '@adonisjs/core/http'
import type {
  AllyDriverContract,
  AllyUserContract,
  ApiRequestContract,
  Oauth2DriverConfig,
  RedirectRequestContract,
} from '@adonisjs/ally/types'

/**
 * Driver Ally Microsoft Identity (v2.0). Couvre Microsoft 365 (work/school)
 * et les comptes personnels (Outlook, Hotmail, Live) — choix du tenant
 * via la prop `tenant` (défaut: `common`).
 *
 * Endpoints :
 *  - Authorize : https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
 *  - Token     : https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
 *  - User info : https://graph.microsoft.com/v1.0/me (retourne id, displayName,
 *                mail, userPrincipalName, givenName, surname)
 *
 * Tenants possibles :
 *  - `common`         : tout (work/school + personal). Notre défaut.
 *  - `organizations`  : work/school uniquement (Microsoft 365 strict)
 *  - `consumers`      : personal uniquement
 *  - `<tenant-id>`    : un tenant Azure AD spécifique
 */

export type MicrosoftDriverAccessToken = {
  token: string
  type: 'bearer'
  refreshToken?: string
  expiresIn?: number
  expiresAt?: Date
  idToken?: string
}

export type MicrosoftDriverScopes =
  | 'openid'
  | 'profile'
  | 'email'
  | 'offline_access'
  | 'User.Read'
  | 'User.ReadBasic.All'

export type MicrosoftDriverConfig = Oauth2DriverConfig & {
  scopes?: MicrosoftDriverScopes[]
  /** common | organizations | consumers | <tenant-id>. Défaut: common. */
  tenant?: string
  /** Force l'écran de sélection de compte. Pratique en dev. */
  prompt?: 'login' | 'none' | 'consent' | 'select_account'
}

type MicrosoftGraphMeResponse = {
  id: string
  displayName: string | null
  givenName: string | null
  surname: string | null
  mail: string | null
  userPrincipalName: string | null
}

export class MicrosoftDriver
  extends Oauth2Driver<MicrosoftDriverAccessToken, MicrosoftDriverScopes>
  implements AllyDriverContract<MicrosoftDriverAccessToken, MicrosoftDriverScopes>
{
  protected accessTokenUrl: string
  protected authorizeUrl: string
  protected userInfoUrl = 'https://graph.microsoft.com/v1.0/me'

  protected codeParamName = 'code'
  protected errorParamName = 'error'
  protected stateCookieName = 'microsoft_oauth_state'
  protected stateParamName = 'state'
  protected scopeParamName = 'scope'
  protected scopesSeparator = ' '

  constructor(
    ctx: HttpContext,
    public config: MicrosoftDriverConfig
  ) {
    super(ctx, config)
    const tenant = config.tenant || 'common'
    this.accessTokenUrl = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`
    this.authorizeUrl = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`
    this.loadState()
  }

  protected configureRedirectRequest(request: RedirectRequestContract<MicrosoftDriverScopes>) {
    request.scopes(this.config.scopes || ['openid', 'profile', 'email', 'User.Read'])
    request.param('response_type', 'code')
    request.param('response_mode', 'query')
    if (this.config.prompt) {
      request.param('prompt', this.config.prompt)
    }
  }

  /** access_denied / consent_required / interaction_required → l'user a annulé. */
  accessDenied(): boolean {
    const error = this.getError()
    if (!error) return false
    return ['access_denied', 'consent_required', 'interaction_required'].includes(error)
  }

  protected getAuthenticatedRequest(url: string, token: string) {
    const request = this.httpClient(url)
    request.header('Authorization', `Bearer ${token}`)
    request.header('Accept', 'application/json')
    request.parseAs('json')
    return request
  }

  /**
   * GET /me sur Microsoft Graph. `mail` peut être null pour des comptes
   * perso → fallback sur `userPrincipalName` (qui est toujours rempli et
   * ressemble à un email).
   */
  protected async getUserInfo(
    token: string,
    callback?: (request: ApiRequestContract) => void
  ): Promise<Omit<AllyUserContract<MicrosoftDriverAccessToken>, 'token'>> {
    const request = this.getAuthenticatedRequest(this.userInfoUrl, token)
    if (typeof callback === 'function') callback(request)

    const body = (await request.get()) as MicrosoftGraphMeResponse
    const email = body.mail ?? body.userPrincipalName
    return {
      id: body.id,
      nickName: body.displayName ?? body.givenName ?? email ?? body.id,
      name: body.displayName ?? [body.givenName, body.surname].filter(Boolean).join(' ') ?? '',
      email: email,
      // Microsoft Graph ne renvoie pas explicitement un état de vérification.
      // On considère l'email vérifié (Microsoft contrôle ses propres domaines).
      emailVerificationState: 'verified' as const,
      avatarUrl: null,
      original: body,
    }
  }

  async user(
    callback?: (request: ApiRequestContract) => void
  ): Promise<AllyUserContract<MicrosoftDriverAccessToken>> {
    const token = await this.accessToken(callback)
    const user = await this.getUserInfo(token.token, callback)
    return { ...user, token }
  }

  async userFromToken(
    token: string,
    callback?: (request: ApiRequestContract) => void
  ): Promise<AllyUserContract<{ token: string; type: 'bearer' }>> {
    const user = await this.getUserInfo(token, callback)
    return { ...user, token: { token, type: 'bearer' as const } }
  }
}

/**
 * Helper de config (équivalent de `services.google()` mais pour Microsoft).
 * On l'enregistre dans `config/ally.ts`.
 */
export function microsoftService(config: MicrosoftDriverConfig) {
  return (ctx: HttpContext) => new MicrosoftDriver(ctx, config)
}
