import { flatten, keyBy } from 'lodash';

import { Client } from '../core/client';
import { CompanySearchHit } from '../entities/company-search-hit.entity';
import { MINI_COMPANY_TYPE } from '../entities/linkedin-mini-company.entity';
import { MiniCompany } from '../entities/mini-company.entity';
import { ProfileId } from '../entities/mini-profile.entity';
import { PeopleSearchHit } from '../entities/people-search-hit.entity';
import { GetBlendedSearchResponse } from '../responses/blended-search.reponse.get';
import { CompanySearchScroller } from '../scrollers/company-search.scroller';
import { PeopleSearchScroller } from '../scrollers/people-search.scroller';
import { JobSearchScroller } from '../scrollers/job-search.scroller';
import { LinkedInNetworkType } from '../types/network.enum';
import { PeopleSearchFilters } from '../types/people-search-filters';
import { SearchResultType } from '../types/search-result-type.enum';
import { LinkedInSearchType } from '../types/search-type.enum';
import { JobSearchFilters } from '../types/job-search-filters';
import { getProfilesFromResponse } from './profile.repository';
import { JOB_POSTING_TYPE, LinkedInJobPosting } from '../entities/linkedin-job-posting';
import { BASE_COMPANY_TYPE, LinkedInBaseCompany } from '../entities/linkedin-base-company';
import { JobSearchHit } from '../entities/job-search-hit.entity';

export class SearchRepository {
  client: Client;

  constructor({ client }: { client: Client }) {
    this.client = client;
  }

  searchPeople({
    skip = 0,
    limit = 10,
    filters = {},
    keywords,
  }: {
    skip?: number;
    limit?: number;
    filters?: PeopleSearchFilters;
    keywords?: string;
  } = {}): PeopleSearchScroller {
    return new PeopleSearchScroller({
      skip,
      limit,
      filters,
      keywords,
      fetchPeople: this.fetchPeople.bind(this),
    });
  }

  searchCompanies({
    skip = 0,
    limit = 10,
    keywords,
  }: {
    skip?: number;
    limit?: number;
    keywords?: string;
  } = {}): CompanySearchScroller {
    return new CompanySearchScroller({
      skip,
      limit,
      keywords,
      fetchCompanies: this.fetchCompanies.bind(this),
    });
  }

  searchOwnConnections({
    skip = 0,
    limit = 10,
    filters = {},
    keywords,
  }: {
    skip?: number;
    limit?: number;
    filters?: Omit<PeopleSearchFilters, 'network'>;
    keywords?: string;
  } = {}): PeopleSearchScroller {
    return new PeopleSearchScroller({
      skip,
      limit,
      keywords,
      filters: { ...filters, network: LinkedInNetworkType.F },
      fetchPeople: this.fetchPeople.bind(this),
    });
  }

  searchConnectionsOf({
    profileId,
    skip = 0,
    limit = 10,
    filters = {},
    keywords,
  }: {
    profileId: ProfileId;
    skip?: number;
    limit?: number;
    filters?: Omit<PeopleSearchFilters, 'network' | 'connectionOf'>;
    keywords?: string;
  }): PeopleSearchScroller {
    return new PeopleSearchScroller({
      skip,
      limit,
      keywords,
      filters: { ...filters, connectionOf: profileId, network: LinkedInNetworkType.F },
      fetchPeople: this.fetchPeople.bind(this),
    });
  }

  searchJobs({
    skip = 0,
    limit = 10,
    filters = {},
    keywords,
  }: {
    skip?: number;
    limit?: number;
    filters?: JobSearchFilters;
    keywords?: string;
  } = {}): JobSearchScroller {
    return new JobSearchScroller({
      skip,
      limit,
      filters,
      keywords,
      fetchJobs: this.fetchJobs.bind(this),
    });
  }

  private async fetchPeople({
    skip = 0,
    limit = 10,
    filters = {},
    keywords,
  }: {
    skip?: number;
    limit?: number;
    filters?: PeopleSearchFilters;
    keywords?: string;
  } = {}): Promise<PeopleSearchHit[]> {
    const response = await this.client.request.search.searchBlended({
      keywords,
      skip,
      limit,
      filters: { ...filters, resultType: LinkedInSearchType.PEOPLE },
    });

    const profiles = keyBy(getProfilesFromResponse<GetBlendedSearchResponse>(response), 'entityUrn');
    const searchHits = flatten(
      response.data.elements.filter(e => e.type === SearchResultType.SEARCH_HITS && e.elements).map(e => e.elements!),
    );

    return searchHits.map(searchHit => ({
      ...searchHit,
      profile: profiles[searchHit.targetUrn],
    }));
  }

  private async fetchCompanies({
    skip = 0,
    limit = 10,
    keywords,
  }: {
    skip?: number;
    limit?: number;
    keywords?: string;
  }): Promise<CompanySearchHit[]> {
    const response = await this.client.request.search.searchBlended({
      skip,
      limit,
      keywords,
      filters: { resultType: LinkedInSearchType.COMPANIES },
    });

    const companies = response.included
      .filter(entity => entity.$type === MINI_COMPANY_TYPE)
      .map(company => ({
        ...company,
        companyId: company.entityUrn.replace('urn:li:fs_miniCompany:', ''),
      })) as MiniCompany[];

    const companiesByUrn = keyBy(companies, 'entityUrn');
    const searchHits = flatten(
      response.data.elements.filter(e => e.type === SearchResultType.SEARCH_HITS && e.elements).map(e => e.elements!),
    );

    return searchHits.map(searchHit => ({
      ...searchHit,
      company: companiesByUrn[searchHit.targetUrn],
    }));
  }

  private async fetchJobs({
    skip = 0,
    limit = 10,
    filters = {},
    keywords,
  }: {
    skip?: number;
    limit?: number;
    filters?: JobSearchFilters;
    keywords?: string;
  } = {}): Promise<JobSearchHit[]> {
    const response = await this.client.request.search.searchJobs({
      filters,
      keywords,
      skip,
      limit,
    });

    const jobPostings = response?.included?.filter(element => element.$type === JOB_POSTING_TYPE) as LinkedInJobPosting[];
    const companies = response?.included?.filter(element => element.$type === BASE_COMPANY_TYPE) as LinkedInBaseCompany[];

    const keyedPostings = keyBy(jobPostings, 'entityUrn');
    const keyedCompanies = keyBy(companies, 'entityUrn');

    const searchHits = response?.data?.elements.map(searchHit => {
      const jobPosting = keyedPostings[searchHit.hitInfo.jobPosting];
      const company = keyedCompanies[jobPosting.companyDetails.company];

      const populatedPosting = {
        ...jobPosting,
        companyDetails: { ...jobPosting.companyDetails, company },
      };

      return {
        ...searchHit,
        hitInfo: {
          ...searchHit.hitInfo,
          jobPosting: populatedPosting,
        },
      };
    });

    return searchHits;
  }
}