import { renderToStaticMarkup } from "react-dom/server";
import DetectionMap from "@/features/i18n/attachments-detection-map.json";
import i18n from "@/features/i18n/initI18n";
import z from "zod";
import { DriveFile } from "../forms/components/message-form/drive-attachment-picker";
import { handle } from "./errors";
import { getBlobDownloadRetrieveUrl } from "@/features/api/gen/blob/blob";

/**
 * Decode HTML entities produced by renderToStaticMarkup in attribute values.
 * &amp; must be decoded last to avoid double-decoding (e.g. &amp;lt; → &lt; → <).
 */
const decodeHtmlEntities = (str: string): string =>
    str.replace(/&lt;/g, '<')
       .replace(/&gt;/g, '>')
       .replace(/&quot;/g, '"')
       .replace(/&#x27;/g, "'")
       .replace(/&amp;/g, '&');

type ImapConfig = {
    host: string;
    port: number;
    use_ssl: boolean;
}

export const IMAP_DOMAIN_REGEXES = new Map<string, string>([
    ["orange", "orange\.fr"],
    ["wanadoo", "wanadoo\.fr"],
    ["gmail", "(gmail\.com|googlemail\.com)"],
    ["yahoo", "yahoo\.(?:[a-z]{2,4}|[a-z]{2}\.[a-z]{2})"],
]);

export const SUPPORTED_IMAP_DOMAINS = new Map<string, ImapConfig>([
    [IMAP_DOMAIN_REGEXES.get("orange")!, { host: "imap.orange.fr", port: 993, use_ssl: true }],
    [IMAP_DOMAIN_REGEXES.get("wanadoo")!, { host: "imap.orange.fr", port: 993, use_ssl: true }],
    [IMAP_DOMAIN_REGEXES.get("gmail")!, { host: "imap.gmail.com", port: 993, use_ssl: true }],
    [IMAP_DOMAIN_REGEXES.get("yahoo")!, { host: "imap.mail.yahoo.com", port: 993, use_ssl: true }],
]);

/* /!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\
   DO NOT EDIT EXISTING VALUE OF `ATTACHMENT_SEPARATORS`, ADD A NEW ONE
   If you want to change the separator, you must add a new value in the array
   Otherwise, previous messages will not be able to be parsed correctly
   /!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\ */
export const ATTACHMENT_SEPARATORS = [
    '---------- Drive attachments ----------',
    '---------- Fichiers joints ----------',
    '---------- Drive-bijlagen ----------',
];

// Active separator used when sending a new message, keyed by i18n language code.
// Unknown languages fall back to the legacy English value (first entry above).
const ATTACHMENT_SEPARATORS_BY_LANG: Record<string, string> = {
    'en-US': '---------- Drive attachments ----------',
    'fr-FR': '---------- Fichiers joints ----------',
    'nl-NL': '---------- Drive-bijlagen ----------',
};

const getAttachmentSeparator = (): string =>
    ATTACHMENT_SEPARATORS_BY_LANG[i18n.language] ?? ATTACHMENT_SEPARATORS[0];

/** An helper which aims to gather all utils related write and send a message */
class MailHelper {

    /**
     * Replace blob download URLs in HTML with cid: references for email embedding.
     * This converts image sources from API URLs to Content-ID references
     * that email clients can resolve using the MIME multipart/related structure.
     *
     * The URL pattern is derived from the Orval-generated getBlobDownloadRetrieveUrl
     * so it stays in sync with the API spec.
     */
    static replaceBlobUrlsWithCid(html: string): string {
        // Use the Orval-generated URL function with a placeholder to derive the pattern
        const placeholder = '__BLOB_ID__';
        const urlTemplate = getBlobDownloadRetrieveUrl(placeholder);
        // Escape regex special chars in the template, then replace the placeholder with a capture group
        const pattern = urlTemplate
            .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
            .replace(placeholder, '([a-f0-9-]+)');
        // Allow an optional origin prefix (full URLs from getRequestUrl)
        const regex = new RegExp(`(?:https?://[^/]+)?${pattern}`, 'g');
        return html.replace(regex, 'cid:$1');
    }

    /**
     * Prefix the subject of a message if it doesn't already start with the prefix.
     */
    static prefixSubjectIfNeeded(subject: string, prefix: string = 'Re:') {
        return subject.startsWith(prefix) ? subject : `${prefix} ${subject}`;
    }

    /**
     * Parse a string of recipients separated by commas
     * and return an array of recipients.
     */
    static parseRecipients(recipients: string) {
        return recipients.split(',').map(recipient => recipient.trim());
    }

    /**
     * Validate an array of recipients, all values must be valid email addresses.
     */
    static areRecipientsValid(recipients: string[] | undefined = [], required: boolean = true) {
        if (required && (recipients.length === 0)) {
            return false;
        }
        if (!recipients.every(r => this.isValidEmail(r))) {
            return false;
        }
        return true;
    }

    /**
     * Test if an email address is valid.
     */
    static isValidEmail(email: string): boolean {
        return z.email().safeParse(email).success;
    }

    /**
     * Get the domain from an email address.
     */
    static getDomainFromEmail(email: string) {
        if (!this.isValidEmail(email)) return undefined;
        return email.split('@')[1];
    }

    /**
     * Get the IMAP config for a given email address
     * if the domain is a supported one (see SUPPORTED_IMAP_DOMAINS)
     */
    static getImapConfigFromEmail(email: string): ImapConfig | undefined {
        const domain = this.getDomainFromEmail(email);
        if (!domain) return undefined;

        return Array
            .from(SUPPORTED_IMAP_DOMAINS.entries())
            .find(([regex]) => new RegExp(`^${regex}$`).test(domain))?.[1];
    }

    /**
     * Get all keywords for attachment detection from the detection map.
     */
    static getAttachmentKeywords(detectionMap: Record<string, Record<string, string[]>>): string[] {
        const allKeywords = new Set<string>();
        Object.values(detectionMap).forEach((langObj) => {
            Object.values(langObj).forEach((arr) => {
                (arr as string[]).forEach((kw) => allKeywords.add(kw.toLowerCase()));
            });
        });
        return Array.from(allKeywords);
    }

    /**
     * Check if any attachment keyword is mentioned in the draft text.
     */
    static areAttachmentsMentionedInDraft(draftText: string = ''): boolean {
        const patterns = MailHelper.getAttachmentKeywords(DetectionMap);
        return patterns.some((pattern) => {
            const isRegex = pattern.startsWith('/') && pattern.endsWith('/');
            if (isRegex) {
                try {
                    return new RegExp(pattern.slice(1, -1), 'i').test(draftText);
                } catch (error) {
                    handle(new Error(`Invalid regex pattern "${pattern}".`), { extra: { error } });
                    return false;
                }
            }
            return draftText.toLowerCase().includes(pattern);
        });
    }

    /**
     * Attach drive attachments to a draft.
     * Attachments are serialized as a JSON string and appended to the draft.
     */
    static attachDriveAttachmentsToDraft(draft: string = '', attachments: DriveFile[] = []) {
        if (attachments.length === 0) return draft;
        return draft
        + getAttachmentSeparator()
        + JSON.stringify(attachments);
    }

    /**
     * Attach drive attachments to a text body.
     * Append attachments as a list of markdown links [name](url).
     */
    static attachDriveAttachmentsToTextBody(textBody: string = '', attachments: DriveFile[] = []) {
        if (attachments.length === 0) return textBody;
        return textBody
        + `\n${getAttachmentSeparator()}\n`
        + attachments.map(a =>
            `- [${a.name}](${a.url})`
        ).join('\n')
        + '\n\n';
    }

    /**
     * Attach drive attachments to a html body.
     * Append attachments as a list of html links <a href="url">name</a> with data attributes.
     */
    static attachDriveAttachmentsToHtmlBody(htmlBody: string = '', attachments: DriveFile[] = []) {
        if (attachments.length === 0) return htmlBody;
        return htmlBody
        + `\n${getAttachmentSeparator()}\n`
        + renderToStaticMarkup(
            <ul>
                {attachments.map((a) => (
                    <li key={a.id}>
                        <a className="drive-attachment" href={a.url} data-id={a.id} data-name={a.name} data-type={a.type} data-size={String(a.size)} data-created_at={a.created_at}>{a.name}</a>
                    </li>
                ))}
            </ul>
        )
        + '\n\n';
    }

    /**
     * Extract drive attachments from a draft.
     */
    static extractDriveAttachmentsFromDraft(draft: string = ''): [string, DriveFile[]] {
        const [draftBody, driveAttachments = '[]'] = draft.split(new RegExp(`${ATTACHMENT_SEPARATORS.join('|')}`, 's'));
        let attachments = [];
        try {
            attachments = JSON.parse(driveAttachments);
        } catch (error) {
            handle(new Error('Cannot parse drive attachments.'), { extra: { error } });
        }
        return [draftBody, attachments];
    }

    /**
     * Extract drive attachments from text body.
     */
    static extractDriveAttachmentsFromTextBody(text: string = ''): [string, Pick<DriveFile, 'name' | 'url'>[]] {
        const regex = new RegExp(`\n(${ATTACHMENT_SEPARATORS.join('|')})[\n\r]*(.*)[\n\r]*`, 's');
        const matches = text.match(regex);
        if (!matches) return [text, []];

        const rawDriveAttachments = matches[2];
        const driveAttachments = rawDriveAttachments.split('\n').map(a => {
            const match = a.match(/^- \[(.*)\]\((.*)\)$/);
            if (!match) return undefined;
            return { name: match[1], url: match[2] };
        }).filter(a => a !== undefined);
        return [text.replace(regex, '').trim(), driveAttachments];
    }

    /**
     * Convert a data URL (base64-encoded) to a File object.
     * Returns null if the input is not a valid image data URL.
     */
    static dataUrlToFile(dataUrl: string, filename: string): File | null {
        const match = dataUrl.match(/^data:(image\/[\w+.-]+);base64,(.+)$/);
        if (!match) return null;

        const [, mimeType, base64Data] = match;
        try {
            const binaryString = atob(base64Data);
            const bytes = new Uint8Array(binaryString.length);
            for (let i = 0; i < binaryString.length; i++) {
                bytes[i] = binaryString.charCodeAt(i);
            }
            return new File([bytes], filename, { type: mimeType });
        } catch {
            return null;
        }
    }

    /**
     * Extract drive attachments from html body.
     */
    static extractDriveAttachmentsFromHtmlBody(html: string = ''): [string, DriveFile[]] {
        const regex = new RegExp(`(${ATTACHMENT_SEPARATORS.join('|')})[\n\r]*<ul>\s*(.*?)\s*</ul>[\n\r]*`, 's');
        const matches = html.match(regex);
        if (!matches) return [html, []];

        // Join the attachment parts and parse anchor elements
        const attachments: DriveFile[] = [];

        // Parse anchor elements with drive-attachment class
        const anchorRegex = /<a[^>]*class="drive-attachment"[^>]*>.*?<\/a>/g;
        let anchorMatch;

        while ((anchorMatch = anchorRegex.exec(matches[2])) !== null) {
            const anchorElement = anchorMatch[0];

            // Extract data attributes
            const extractDataAttribute = (attr: string): string | null => {
                const regex = new RegExp(`data-${attr}="([^"]*)"`, 'i');
                const anchorMatch = anchorElement.match(regex);
                return anchorMatch ? decodeHtmlEntities(anchorMatch[1]) : null;
            };

            const id = extractDataAttribute('id');
            const name = extractDataAttribute('name');
            const type = extractDataAttribute('type');
            const sizeStr = extractDataAttribute('size');
            const created_at = extractDataAttribute('created_at');

            // Extract href attribute
            const hrefMatch = anchorElement.match(/href="([^"]*)"/);
            const url = hrefMatch ? decodeHtmlEntities(hrefMatch[1]) : '';

            if (id && name && url) {
                attachments.push({
                    id,
                    name,
                    type: type || 'application/octet-stream',
                    size: parseInt(sizeStr || '0', 10),
                    created_at: created_at || '',
                    url,
                });
            } else {
                handle(new Error('Cannot extract drive attachment from anchor element.'), { extra: { anchorElement } });
            }
        }

        return [html.replace(regex, '').trim(), attachments];
    }
}

export default MailHelper;
