import { AppError } from '../utils/AppError';

export interface LinkedInExperience {
  company: string;
  jobTitle?: string;
}

export interface LinkedInProfileData {
  fullName?: string;
  headline?: string;
  /** LinkedIn About section only */
  bio?: string;
  jobTitle?: string;
  company?: string;
  location?: string;
  education?: string;
  experiences?: LinkedInExperience[];
  avatarUrl?: string;
  fetchWarnings?: string[];
}

const FETCH_USER_AGENTS = [
  'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
  'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)',
  'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)',
  'Twitterbot/1.0',
  'LinkedInBot/1.0 (compatible; Mozilla/5.0; +https://www.linkedin.com/)',
];

const FETCH_TIMEOUT_MS = 20_000;
const RETRYABLE_HTTP = new Set([429, 999, 503]);

type SchemaNode = Record<string, unknown>;

export class LinkedInProfileFetcher {
  normalizeUrl(rawUrl: string): string {
    let url = rawUrl.trim();
    if (!/^https?:\/\//i.test(url)) {
      url = `https://${url}`;
    }

    const parsed = new URL(url);
    if (!/^(www\.)?linkedin\.com$/i.test(parsed.hostname)) {
      throw new AppError('validation_error', 400, 'Must be a LinkedIn profile URL.');
    }

    const match = parsed.pathname.match(/\/(in|pub)\/([^/?#]+)/i);
    if (!match) {
      throw new AppError('validation_error', 400, 'Must be a LinkedIn profile URL (linkedin.com/in/…).');
    }

    const slug = decodeURIComponent(match[2]).replace(/\/$/, '');
    return `https://www.linkedin.com/${match[1].toLowerCase()}/${slug}/`;
  }

  async fetchProfile(linkedinUrl: string): Promise<LinkedInProfileData> {
    const url = this.normalizeUrl(linkedinUrl);
    let lastError: Error | null = null;
    let partial: LinkedInProfileData | null = null;

    for (let index = 0; index < FETCH_USER_AGENTS.length; index++) {
      const userAgent = FETCH_USER_AGENTS[index];
      try {
        if (index > 0) {
          await this.sleep(800 * index);
        }

        const html = await this.fetchHtml(url, userAgent);
        const meta = this.extractOpenGraphTags(html);
        const fromTitle = this.parseOgTitle(meta.title ?? '');
        const fromOg = this.parseOpenGraph(meta, fromTitle.headline);
        const fromJsonLd = this.extractPersonSchema(html, url);

        const about = this.pickBestAbout(
          this.collectAboutCandidates(html, meta.description, fromTitle.headline),
        );

        const merged = this.mergeProfileData(fromJsonLd, fromOg, about);
        if (this.hasProfileContent(merged)) {
          if (merged.bio && merged.bio.length >= 80) {
            return merged;
          }
          partial = merged;
        }
      } catch (err) {
        lastError = err instanceof Error ? err : new Error(String(err));
      }
    }

    const proxycurl = await this.fetchFromProxycurl(url);
    if (proxycurl && this.hasProfileContent(proxycurl)) {
      return this.mergeWithFallback(partial, proxycurl);
    }

    if (partial) {
      return {
        ...partial,
        fetchWarnings: [
          'LinkedIn About section was not found in the public page. Paste your About text below or configure PROXYCURL_API_KEY for full imports.',
        ],
      };
    }

    throw new AppError(
      'linkedin_fetch_failed',
      502,
      lastError?.message ??
        'Could not load profile data from LinkedIn. Check the URL is public and try again.',
    );
  }

  parseOpenGraph(
    meta: { title?: string; description?: string; image?: string },
    titleHeadline?: string,
  ): LinkedInProfileData {
    const fromTitle = this.parseOgTitle(meta.title ?? '');
    const fromDescription = this.parseOgDescription(meta.description ?? '', titleHeadline ?? fromTitle.headline);
    const headline = titleHeadline ?? fromTitle.headline ?? fromDescription.headline;
    const roleFromHeadline = headline ? this.parseRoleFromHeadline(headline) : {};

    return {
      fullName: fromTitle.fullName,
      headline,
      bio: fromDescription.bio,
      jobTitle: fromDescription.jobTitle ?? roleFromHeadline.jobTitle,
      company: fromDescription.company ?? roleFromHeadline.company,
      location: fromDescription.location,
      education: fromDescription.education,
      avatarUrl: meta.image ? this.decodeHtmlEntities(meta.image) : undefined,
    };
  }

  private hasProfileContent(data: LinkedInProfileData): boolean {
    return Boolean(
      data.fullName ||
        data.headline ||
        data.bio ||
        data.jobTitle ||
        data.company ||
        data.location,
    );
  }

  private mergeProfileData(
    jsonLd: LinkedInProfileData,
    og: LinkedInProfileData,
    about?: string,
  ): LinkedInProfileData {
    const headline = og.headline ?? jsonLd.headline;
    const roleFromHeadline = headline ? this.parseRoleFromHeadline(headline) : {};

    const bio = this.pickBestAbout(
      [about, jsonLd.bio, og.bio].filter((value): value is string => Boolean(value?.trim())),
    );

    const jobTitle = roleFromHeadline.jobTitle ?? jsonLd.jobTitle ?? og.jobTitle;
    const company = roleFromHeadline.company ?? jsonLd.company ?? og.company;

    let resolvedHeadline = headline;
    if (!resolvedHeadline && jobTitle && company) {
      resolvedHeadline = `${jobTitle} at ${company}`;
    } else if (!resolvedHeadline) {
      resolvedHeadline = jobTitle ?? company;
    }

    return {
      fullName: jsonLd.fullName ?? og.fullName,
      headline: resolvedHeadline,
      bio,
      jobTitle,
      company,
      location: jsonLd.location ?? og.location,
      education: jsonLd.education ?? og.education,
      experiences: jsonLd.experiences?.length ? jsonLd.experiences : og.experiences,
      avatarUrl: jsonLd.avatarUrl ?? og.avatarUrl,
    };
  }

  private collectAboutCandidates(
    html: string,
    ogDescription?: string,
    titleHeadline?: string,
  ): string[] {
    const candidates: string[] = [];

    for (const description of this.extractAllPersonDescriptions(html)) {
      candidates.push(description);
    }

    if (ogDescription) {
      const parsed = this.parseOgDescription(ogDescription, titleHeadline);
      if (parsed.bio) candidates.push(parsed.bio);
    }

    const metaDescription = this.extractMetaDescription(html);
    if (metaDescription && metaDescription !== ogDescription) {
      const parsed = this.parseOgDescription(metaDescription, titleHeadline);
      if (parsed.bio) candidates.push(parsed.bio);
    }

    return candidates;
  }

  private extractAllPersonDescriptions(html: string): string[] {
    const blocks = html.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/gi);
    if (!blocks) return [];

    const descriptions: string[] = [];

    for (const block of blocks) {
      const jsonText = block.replace(/<\/?script[^>]*>/gi, '').trim();
      try {
        const parsed = JSON.parse(jsonText) as SchemaNode | SchemaNode[];
        const persons: SchemaNode[] = [];
        this.collectPersonNodes(parsed, persons);

        for (const person of persons) {
          if (typeof person.description === 'string' && person.description.trim()) {
            descriptions.push(this.normalizeAboutText(person.description));
          }
        }
      } catch {
        continue;
      }
    }

    return descriptions;
  }

  private pickBestAbout(candidates: string[]): string | undefined {
    const normalized = candidates
      .map((value) => this.normalizeAboutText(value))
      .filter((value) => value.length >= 40);

    if (!normalized.length) return undefined;

    return normalized.sort((a, b) => b.length - a.length)[0];
  }

  private extractPersonSchema(html: string, profileUrl: string): LinkedInProfileData {
    const blocks = html.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/gi);
    if (!blocks) return {};

    const persons: SchemaNode[] = [];

    for (const block of blocks) {
      const jsonText = block.replace(/<\/?script[^>]*>/gi, '').trim();
      try {
        const parsed = JSON.parse(jsonText) as SchemaNode | SchemaNode[];
        this.collectPersonNodes(parsed, persons);
      } catch {
        continue;
      }
    }

    if (!persons.length) return {};

    const profileSlug = profileUrl.replace(/\/$/, '').toLowerCase();
    const best = persons.sort(
      (a, b) => this.scorePersonNode(b, profileSlug) - this.scorePersonNode(a, profileSlug),
    )[0];

    return this.parsePersonNode(best);
  }

  private collectPersonNodes(data: SchemaNode | SchemaNode[], persons: SchemaNode[]): void {
    if (Array.isArray(data)) {
      for (const item of data) {
        this.collectPersonNodes(item, persons);
      }
      return;
    }

    if (data['@type'] === 'Person') {
      persons.push(data);
    }

    const graph = data['@graph'];
    if (Array.isArray(graph)) {
      this.collectPersonNodes(graph, persons);
    }

    for (const value of Object.values(data)) {
      if (value && typeof value === 'object') {
        if (Array.isArray(value)) {
          this.collectPersonNodes(value, persons);
        } else if ((value as SchemaNode)['@type'] !== 'Person') {
          this.collectPersonNodes(value as SchemaNode, persons);
        }
      }
    }
  }

  private scorePersonNode(person: SchemaNode, profileSlug: string): number {
    let score = 0;

    const description = typeof person.description === 'string' ? person.description.trim() : '';
    if (description) score += 200 + description.length;

    const worksFor = person.worksFor;
    if (Array.isArray(worksFor) && worksFor.length) score += 100 + worksFor.length * 10;

    if (person.address) score += 30;
    if (person.image) score += 20;
    if (person.alumniOf) score += 15;
    if (person.jobTitle) score += 15;

    const url = typeof person.url === 'string' ? person.url.toLowerCase() : '';
    const sameAs = typeof person.sameAs === 'string' ? person.sameAs.toLowerCase() : '';
    if (url.includes(profileSlug) || sameAs.includes(profileSlug)) score += 500;

    return score;
  }

  private parsePersonNode(person: SchemaNode): LinkedInProfileData {
    const fullName = typeof person.name === 'string' ? person.name.trim() : undefined;
    const about =
      typeof person.description === 'string'
        ? this.normalizeAboutText(person.description)
        : undefined;

    const experiences = this.readExperiences(person.worksFor);
    const primaryExperience = experiences[0];
    const location = this.readLocation(person.address);
    const education = this.readEducation(person.alumniOf);
    const avatarUrl = this.readImage(person.image);

    const roleTitles = this.readJobTitleList(person.jobTitle);
    const jobTitle = primaryExperience?.jobTitle ?? roleTitles[0];
    const company = primaryExperience?.company;

    return {
      fullName,
      bio: about,
      jobTitle,
      company,
      location,
      education,
      experiences,
      avatarUrl,
    };
  }

  private readJobTitleList(value: unknown): string[] {
    if (typeof value === 'string' && value.trim()) {
      return [value.trim()];
    }
    if (Array.isArray(value)) {
      return value
        .filter((item): item is string => typeof item === 'string' && Boolean(item.trim()))
        .map((item) => item.trim());
    }
    return [];
  }

  private readExperiences(value: unknown): LinkedInExperience[] {
    if (!Array.isArray(value)) return [];

    return value
      .map((item) => {
        if (typeof item === 'string' && item.trim()) {
          return { company: item.trim() };
        }
        if (!item || typeof item !== 'object') return null;

        const org = item as SchemaNode;
        const company = typeof org.name === 'string' ? org.name.trim() : '';
        if (!company) return null;

        let jobTitle: string | undefined;
        const member = org.member;
        if (member && typeof member === 'object') {
          const role = (member as SchemaNode).title;
          if (typeof role === 'string' && role.trim()) {
            jobTitle = role.trim();
          }
        }

        return { company, jobTitle };
      })
      .filter((item): item is LinkedInExperience => item !== null);
  }

  private readLocation(value: unknown): string | undefined {
    if (!value || typeof value !== 'object') return undefined;
    const address = value as SchemaNode;
    if (typeof address.addressLocality === 'string') {
      return address.addressLocality.trim();
    }
    return undefined;
  }

  private readEducation(value: unknown): string | undefined {
    if (!Array.isArray(value)) return undefined;

    const schools = value
      .map((item) => {
        if (typeof item === 'string') return item.trim();
        if (item && typeof item === 'object') {
          const org = item as SchemaNode;
          if (org['@type'] && org['@type'] !== 'EducationalOrganization') return '';
          if (typeof org.name === 'string') return org.name.trim();
        }
        return '';
      })
      .filter(Boolean);

    return schools.length ? schools.join(', ') : undefined;
  }

  private readImage(value: unknown): string | undefined {
    if (typeof value === 'string' && value.trim()) {
      return value.trim();
    }
    if (value && typeof value === 'object') {
      const image = value as SchemaNode;
      if (typeof image.contentUrl === 'string') {
        return image.contentUrl.trim();
      }
      if (typeof image.url === 'string') {
        return image.url.trim();
      }
    }
    return undefined;
  }

  private parseRoleFromHeadline(headline: string): Pick<LinkedInProfileData, 'jobTitle' | 'company'> {
    const cleaned = headline.trim();
    const atMatch = cleaned.match(/^(.+?)\s+at\s+(.+)$/i);
    if (atMatch) {
      return { jobTitle: atMatch[1].trim(), company: atMatch[2].trim() };
    }

    const pipeMatch = cleaned.match(/^(.+?)\s*\|\s*(.+)$/);
    if (pipeMatch) {
      return { jobTitle: pipeMatch[1].trim(), company: pipeMatch[2].trim() };
    }

    return {};
  }

  private parseOgDescription(description: string, titleHeadline?: string): LinkedInProfileData {
    const parts = description
      .split('·')
      .map((part) => part.trim())
      .filter(Boolean);

    let headline = titleHeadline;
    let about: string | undefined;
    let jobTitle: string | undefined;
    let company: string | undefined;
    let location: string | undefined;
    let education: string | undefined;
    const contentParts: string[] = [];

    for (const part of parts) {
      const experienceMatch = part.match(/^Experience:\s*(.+)$/i);
      if (experienceMatch) {
        company = experienceMatch[1].trim();
        continue;
      }

      const locationMatch = part.match(/^Location:\s*(.+)$/i);
      if (locationMatch) {
        location = locationMatch[1]
          .replace(/\s*\d+\+?\s*connections on LinkedIn.*/i, '')
          .replace(/\.\s*View .+ profile on LinkedIn.*/i, '')
          .trim();
        continue;
      }

      const educationMatch = part.match(/^Education:\s*(.+)$/i);
      if (educationMatch) {
        education = educationMatch[1].trim();
        continue;
      }

      if (/View .+ profile on LinkedIn/i.test(part)) continue;
      if (/^\d+\+?\s*connections on LinkedIn/i.test(part)) continue;

      contentParts.push(part);
    }

    if (contentParts.length === 1) {
      const only = this.normalizeAboutText(contentParts[0]);
      if (this.looksLikeAboutProse(only)) {
        about = only;
      } else if (!headline) {
        headline = only;
        const role = this.parseRoleFromHeadline(only);
        jobTitle = role.jobTitle;
        company = role.company;
      }
    } else if (contentParts.length > 1) {
      const normalized = contentParts.map((part) => this.normalizeAboutText(part));
      const first = normalized[0];
      const rest = normalized.slice(1);

      if (!this.looksLikeAboutProse(first) && rest.some((part) => part.length > first.length)) {
        if (!headline) {
          headline = first;
          const role = this.parseRoleFromHeadline(first);
          jobTitle = role.jobTitle;
          company = role.company;
        }
        about = rest.join('\n\n');
      } else {
        about = normalized.join('\n\n');
      }
    }

    if (about) {
      about = this.normalizeAboutText(about);
    }

    if (company && !jobTitle && headline) {
      const role = this.parseRoleFromHeadline(headline);
      jobTitle = role.jobTitle;
      if (role.company) company = role.company;
    }

    const experiences = company ? [{ company, jobTitle }] : undefined;

    return { headline, bio: about, jobTitle, company, location, education, experiences };
  }

  private looksLikeAboutProse(text: string): boolean {
    const trimmed = text.trim();
    if (trimmed.length >= 180) return true;
    const sentenceCount = (trimmed.match(/[.!?](?:\s|$)/g) ?? []).length;
    return sentenceCount >= 2 || (trimmed.length >= 80 && sentenceCount >= 1);
  }

  private normalizeAboutText(value: string): string {
    let text = this.decodeHtmlEntities(value);
    text = text
      .replace(/<br\s*\/?>/gi, '\n\n')
      .replace(/<\/p>\s*<p[^>]*>/gi, '\n\n')
      .replace(/<\/li>\s*<li[^>]*>/gi, '\n')
      .replace(/<[^>]+>/g, '')
      .replace(/\u00a0/g, ' ')
      .replace(/\r\n/g, '\n')
      .replace(/[ \t]+\n/g, '\n')
      .replace(/\n{3,}/g, '\n\n')
      .replace(/[ \t]{2,}/g, ' ')
      .trim();

    return text;
  }

  private async fetchHtml(url: string, userAgent: string): Promise<string> {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

    try {
      const response = await fetch(url, {
        signal: controller.signal,
        headers: {
          'User-Agent': userAgent,
          Accept: 'text/html,application/xhtml+xml',
          'Accept-Language': 'en-US,en;q=0.9',
          'Cache-Control': 'no-cache',
        },
        redirect: 'follow',
      });

      if (!response.ok) {
        if (RETRYABLE_HTTP.has(response.status)) {
          throw new AppError(
            'linkedin_fetch_failed',
            502,
            `LinkedIn temporarily blocked the request (HTTP ${response.status}). Try again shortly.`,
          );
        }
        throw new AppError('linkedin_fetch_failed', 502, `LinkedIn returned HTTP ${response.status}.`);
      }

      const html = await response.text();
      if (html.length < 5000 && !html.includes('og:description')) {
        throw new AppError(
          'linkedin_fetch_failed',
          502,
          'LinkedIn returned an incomplete profile page. Try again shortly.',
        );
      }

      return html;
    } catch (err) {
      if (err instanceof AppError) throw err;
      if (err instanceof Error && err.name === 'AbortError') {
        throw new AppError('linkedin_fetch_failed', 504, 'LinkedIn request timed out.');
      }
      throw err;
    } finally {
      clearTimeout(timeout);
    }
  }

  private extractOpenGraphTags(html: string): { title?: string; description?: string; image?: string } {
    return {
      title: this.readMetaContent(html, 'og:title'),
      description: this.readMetaContent(html, 'og:description'),
      image: this.readMetaContent(html, 'og:image'),
    };
  }

  private extractMetaDescription(html: string): string | undefined {
    return this.readMetaContent(html, 'description');
  }

  private readMetaContent(html: string, property: string): string | undefined {
    const patterns = [
      new RegExp(`property="${property}"\\s+content="([^"]*)"`, 'i'),
      new RegExp(`content="([^"]*)"\\s+property="${property}"`, 'i'),
      new RegExp(`name="${property}"\\s+content="([^"]*)"`, 'i'),
      new RegExp(`content="([^"]*)"\\s+name="${property}"`, 'i'),
    ];

    for (const pattern of patterns) {
      const match = html.match(pattern);
      if (match?.[1]) {
        return this.decodeHtmlEntities(match[1]);
      }
    }
    return undefined;
  }

  private parseOgTitle(title: string): Pick<LinkedInProfileData, 'fullName' | 'headline'> {
    const cleaned = title.replace(/\s*\|\s*LinkedIn\s*$/i, '').trim();
    if (!cleaned) return {};

    const dashIndex = cleaned.indexOf(' - ');
    if (dashIndex === -1) {
      return { fullName: cleaned };
    }

    return {
      fullName: cleaned.slice(0, dashIndex).trim(),
      headline: cleaned.slice(dashIndex + 3).trim(),
    };
  }

  private decodeHtmlEntities(value: string): string {
    let text = value;
    for (let i = 0; i < 3; i++) {
      const next = text
        .replace(/&amp;/g, '&')
        .replace(/&quot;/g, '"')
        .replace(/&#39;/g, "'")
        .replace(/&apos;/g, "'")
        .replace(/&#x27;/gi, "'")
        .replace(/&lt;/g, '<')
        .replace(/&gt;/g, '>');
      if (next === text) break;
      text = next;
    }
    return text;
  }

  private mergeWithFallback(
    partial: LinkedInProfileData | null,
    fallback: LinkedInProfileData,
  ): LinkedInProfileData {
    if (!partial) return fallback;

    return {
      fullName: partial.fullName ?? fallback.fullName,
      headline: partial.headline ?? fallback.headline,
      bio: this.pickBestAbout([partial.bio, fallback.bio].filter((v): v is string => Boolean(v?.trim()))),
      jobTitle: partial.jobTitle ?? fallback.jobTitle,
      company: partial.company ?? fallback.company,
      location: partial.location ?? fallback.location,
      education: partial.education ?? fallback.education,
      experiences: partial.experiences?.length ? partial.experiences : fallback.experiences,
      avatarUrl: partial.avatarUrl ?? fallback.avatarUrl,
    };
  }

  private async fetchFromProxycurl(linkedinUrl: string): Promise<LinkedInProfileData | null> {
    const apiKey = process.env.PROXYCURL_API_KEY?.trim();
    if (!apiKey) return null;

    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

    try {
      const response = await fetch(
        `https://nubela.co/proxycurl/api/v2/linkedin?linkedin_profile_url=${encodeURIComponent(linkedinUrl)}`,
        {
          signal: controller.signal,
          headers: { Authorization: `Bearer ${apiKey}` },
        },
      );

      if (!response.ok) return null;

      const data = (await response.json()) as Record<string, unknown>;
      const experiences: LinkedInExperience[] = Array.isArray(data.experiences)
        ? data.experiences.flatMap((item) => {
            if (!item || typeof item !== 'object') return [];
            const exp = item as Record<string, unknown>;
            const company = typeof exp.company === 'string' ? exp.company.trim() : '';
            if (!company) return [];
            const jobTitle = typeof exp.title === 'string' ? exp.title.trim() : undefined;
            return [{ company, jobTitle }];
          })
        : [];

      const education = Array.isArray(data.education)
        ? data.education
            .map((item) => {
              if (!item || typeof item !== 'object') return '';
              const school = (item as Record<string, unknown>).school;
              return typeof school === 'string' ? school.trim() : '';
            })
            .filter(Boolean)
            .join(', ')
        : undefined;

      return {
        fullName: typeof data.full_name === 'string' ? data.full_name : undefined,
        headline: typeof data.headline === 'string' ? data.headline : undefined,
        bio: typeof data.summary === 'string' ? this.normalizeAboutText(data.summary) : undefined,
        jobTitle: typeof data.occupation === 'string' ? data.occupation : undefined,
        company: experiences?.[0]?.company,
        location:
          typeof data.city === 'string' && typeof data.country_full_name === 'string'
            ? `${data.city}, ${data.country_full_name}`
            : typeof data.country_full_name === 'string'
              ? data.country_full_name
              : undefined,
        education,
        experiences: experiences.length ? experiences : undefined,
        avatarUrl: typeof data.profile_pic_url === 'string' ? data.profile_pic_url : undefined,
      };
    } catch {
      return null;
    } finally {
      clearTimeout(timeout);
    }
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}
