import {
  HttpErrorResponse,
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest,
  HttpResponse,
  HttpXsrfTokenExtractor,
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import {
  Observable,
  throwError,
} from 'rxjs';
import {
  catchError,
  tap,
} from 'rxjs/operators';

import { CookieService } from '../services/cookie.service';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
import {
  XSRF_COOKIE,
  XSRF_REQUEST_HEADER,
  XSRF_RESPONSE_HEADER,
} from './xsrf.constants';

/**
 * Custom Http Interceptor intercepting Http Requests & Responses to
 * exchange XSRF/CSRF tokens with the backend.
 *
 * DSpace has a custom XSRF token process in order to support the UI and backend
 * running on entirely separate domains:
 *
 * 1. Backend generates XSRF token & stores in a *server-side* (only) cookie
 *    named DSPACE-XSRF-COOKIE. This cookie is not readable to Angular, but is
 *    returned (by user's browser) on every subsequent request to backend.
 * 2. Backend also sends XSRF token in a header named DSPACE-XSRF-TOKEN to
 *    Angular.
 * 3. This interceptor looks for DSPACE-XSRF-TOKEN header in a response. If
 *    found, its value is saved to a *client-side* (only) cookie named XSRF-TOKEN.
 * 4. Whenever Angular is making a mutating request (POST, PUT, DELETE, etc),
 *    this interceptor checks for that client-side XSRF-TOKEN cookie. If found,
 *    its value is sent to the backend in the X-XSRF-TOKEN header.
 * 5. On backend, the X-XSRF-TOKEN header is received & compared to the current
 *    value of the *server-side* cookie named DSPACE-XSRF-COOKIE. If tokens
 *    match, the request is accepted. If tokens don't match a 403 is returned.
 *
 * In summary, the XSRF token is ALWAYS sent to/from the Angular UI and backend
 * via *headers*. Both the Angular UI and backend have cookies which retain the
 * last known value of the XSRF token for verification purposes.
 */
@Injectable()
export class XsrfInterceptor implements HttpInterceptor {

  constructor(private tokenExtractor: HttpXsrfTokenExtractor, private cookieService: CookieService) {
  }

  /**
     * Intercept http requests and add the XSRF/CSRF token to the X-Forwarded-For header
     * @param httpRequest
     * @param next
     */
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Ensure EVERY request from Angular includes "withCredentials: true".
    // This allows Angular to receive & send cookies via a CORS request (to
    // the backend). ONLY requests with credentials will:
    // 1. Ensure a user's browser sends the server-side XSRF cookie back to
    //    the backend
    // 2. Ensure a user's browser saves changes to the server-side XSRF
    //    cookie (to ensure it is kept in sync with client side cookie)
    req = req.clone({ withCredentials: true });

    // Get request URL
    const reqUrl = req.url.toLowerCase();

    // Get root URL of configured REST API
    const restUrl = new RESTURLCombiner('/').toString().toLowerCase();

    // Skip any non-mutating request. This is because our REST API does NOT
    // require CSRF verification for read-only requests like GET or HEAD
    // Also skip any request which is NOT to our trusted/configured REST API
    if (req.method !== 'GET' && req.method !== 'HEAD' && reqUrl.startsWith(restUrl)) {
      // parse token from XSRF-TOKEN (client-side) cookie
      const token = this.tokenExtractor.getToken() as string;

      // send token in request's X-XSRF-TOKEN header (anti-CSRF security) to backend
      if (token !== null && !req.headers.has(XSRF_REQUEST_HEADER)) {
        req = req.clone({ headers: req.headers.set(XSRF_REQUEST_HEADER, token) });
      }
    }
    // Pass to next interceptor, but intercept EVERY response event as well
    return next.handle(req).pipe(
      // Check event that came back...is it an HttpResponse from backend?
      tap((response) => {
        if (response instanceof HttpResponse) {
          // For every response that comes back, check for the custom
          // DSPACE-XSRF-TOKEN header sent from the backend.
          if (response.headers.has(XSRF_RESPONSE_HEADER)) {
            // value of header is a new XSRF token
            this.saveXsrfToken(response.headers.get(XSRF_RESPONSE_HEADER));
          }
        }
      }),
      catchError((error: unknown) => {
        if (error instanceof HttpErrorResponse) {
          // For every error that comes back, also check for the custom
          // DSPACE-XSRF-TOKEN header sent from the backend.
          if (error.headers.has(XSRF_RESPONSE_HEADER)) {
            // value of header is a new XSRF token
            this.saveXsrfToken(error.headers.get(XSRF_RESPONSE_HEADER));
          }
        }
        // Return error response as is.
        return throwError(error);
      }),
    ) as any;
  }

  /**
     * Save XSRF token found in response
     * @param token token found
     */
  private saveXsrfToken(token: string) {
    // Save token value as a *new* value of our client-side XSRF-TOKEN cookie.
    // This is the cookie that is parsed by Angular's tokenExtractor(),
    // which we will send back in the X-XSRF-TOKEN header per Angular best practices.
    this.cookieService.remove(XSRF_COOKIE);
    this.cookieService.set(XSRF_COOKIE, token);
  }
}
