// lib/gitea-sync.js — Gitea Sync Engine

const SYNC_CONFIG_KEY = 'gitea_config';
const SYNC_STATE_KEY = 'sync_state';
const TEMPLATE_STORAGE_KEY = 'message_templates';
const SHA_CHECK_INTERVAL_MS = 5 * 1000; // 5 seconds — lightweight SHA check
const FULL_SYNC_COOLDOWN_MS = 10 * 1000; // min 10s between full pulls
const SHARED_FOLDER = '_gemeinsam';
const USER_FOLDER = '_benutzer';
const CONFIG_FOLDER = '_config';
const CLIENTS_FOLDER = '_clients';
const SCHLAGWOERTER_CACHE_KEY = 'schlagwoerter_cache';

// ── Gitea API Client ──

class GiteaClient {
    constructor(config) {
        this.baseUrl = config.baseUrl;
        this.owner = config.owner;
        this.repo = config.repo;
        this.branch = config.branch || 'main';
        this.token = config.token;
        this.authorName = config.authorName || '';
        this.authorEmail = config.authorEmail || '';
    }

    get apiBase() {
        return `${this.baseUrl}/api/v1/repos/${this.owner}/${this.repo}`;
    }

    get headers() {
        return {
            'Authorization': `token ${this.token}`,
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        };
    }

    get authorInfo() {
        if (!this.authorName) return {};
        return {
            author: {
                name: this.authorName,
                email: this.authorEmail || `${this.authorName.toLowerCase().replace(/\s+/g, '.')}@local`
            }
        };
    }

    static toBase64(str) {
        return btoa(unescape(encodeURIComponent(str)));
    }

    static fromBase64(b64) {
        if (!b64) return '';
        return decodeURIComponent(escape(atob(b64.replace(/\s/g, ''))));
    }

    encodePath(p) {
        return p.split('/').map(encodeURIComponent).join('/');
    }

    async apiError(method, filepath, res) {
        let detail = '';
        try { const body = await res.json(); detail = body.message || JSON.stringify(body); } catch(_) {}
        throw new Error(`${method} ${filepath}: ${res.status} ${res.statusText}${detail ? ' — ' + detail : ''}`);
    }

    async getFile(filepath) {
        const url = `${this.apiBase}/contents/${this.encodePath(filepath)}?ref=${this.branch}`;
        const res = await fetch(url, { headers: this.headers });
        if (res.status === 404) return null;
        if (!res.ok) await this.apiError('GET', filepath, res);
        return res.json();
    }

    async createFile(filepath, content, message) {
        const url = `${this.apiBase}/contents/${this.encodePath(filepath)}`;
        const res = await fetch(url, {
            method: 'POST',
            headers: this.headers,
            body: JSON.stringify({
                content: GiteaClient.toBase64(content),
                message: message || `Add ${filepath}`,
                branch: this.branch,
                ...this.authorInfo
            })
        });
        if (!res.ok) await this.apiError('POST', filepath, res);
        return res.json();
    }

    async updateFile(filepath, content, sha, message) {
        const url = `${this.apiBase}/contents/${this.encodePath(filepath)}`;
        const res = await fetch(url, {
            method: 'PUT',
            headers: this.headers,
            body: JSON.stringify({
                content: GiteaClient.toBase64(content),
                sha,
                message: message || `Update ${filepath}`,
                branch: this.branch,
                ...this.authorInfo
            })
        });
        if (!res.ok) await this.apiError('PUT', filepath, res);
        return res.json();
    }

    async deleteFile(filepath, sha, message) {
        const url = `${this.apiBase}/contents/${this.encodePath(filepath)}`;
        const res = await fetch(url, {
            method: 'DELETE',
            headers: this.headers,
            body: JSON.stringify({
                sha,
                message: message || `Delete ${filepath}`,
                branch: this.branch,
                ...this.authorInfo
            })
        });
        if (!res.ok) await this.apiError('DELETE', filepath, res);
        return res.json();
    }

    async listDir(dirpath) {
        const pathPart = dirpath ? `/${this.encodePath(dirpath)}` : '';
        const url = `${this.apiBase}/contents${pathPart}?ref=${this.branch}`;
        const res = await fetch(url, { headers: this.headers });
        if (res.status === 404) return [];
        if (!res.ok) await this.apiError('LIST', dirpath, res);
        const result = await res.json();
        return Array.isArray(result) ? result : [];
    }

    async testConnection() {
        const url = `${this.baseUrl}/api/v1/repos/${this.owner}/${this.repo}`;
        const res = await fetch(url, { headers: this.headers });
        if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
        return res.json();
    }

    async getConfig() {
        const data = await this.getFile(`${CONFIG_FOLDER}/abteilungen.json`);
        if (!data) return null;
        try {
            return JSON.parse(GiteaClient.fromBase64(data.content));
        } catch (_) {
            return null;
        }
    }

    async getSchlagwoerter() {
        const data = await this.getFile(`${CONFIG_FOLDER}/schlagwoerter.json`);
        if (!data) return null;
        try {
            return JSON.parse(GiteaClient.fromBase64(data.content));
        } catch (_) {
            return null;
        }
    }
}

// ── Sync Manager ──

class SyncManager {
    constructor() {
        this.client = null;
        this.config = null;
    }

    async init() {
        const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
        this.config = result[SYNC_CONFIG_KEY];
        if (!this.config || !this.config.baseUrl || !this.config.token) {
            this.client = null;
            return false;
        }
        this.client = new GiteaClient(this.config);
        return true;
    }

    get isConfigured() {
        return this.client !== null;
    }

    get department() {
        return this.config?.department || '';
    }

    get authorEmail() {
        return this.config?.authorEmail || '';
    }

    get authorName() {
        return this.config?.authorName || '';
    }

    /**
     * Meldet den eigenen Client-Status (Name, Abteilung, Plugin- + TB-Version)
     * als _clients/<email>.json ins Repo — Basis für die Admin-Übersicht im
     * Web-Editor. Schreibt NUR, wenn sich tatsächlich etwas geändert hat
     * (Plugin-Version, TB-Version, Name oder Abteilung) — sonst kein Commit.
     */
    async reportClientStatus() {
        if (!this.isConfigured) return { success: false, skipped: 'not-configured' };
        const email = this.authorEmail.trim().toLowerCase();
        if (!email) return { success: false, skipped: 'no-email' };

        let tbVersion = '';
        try {
            const info = await browser.runtime.getBrowserInfo();
            tbVersion = [info.name, info.version].filter(Boolean).join(' ');
        } catch (_) {}

        const status = {
            name: this.authorName,
            email,
            department: this.department,
            pluginVersion: browser.runtime.getManifest().version,
            tbVersion,
            lastSeen: new Date().toISOString()
        };

        const filepath = `${CLIENTS_FOLDER}/${email}.json`;
        const body = JSON.stringify(status, null, 2);
        const commitMsg = `Client-Status: ${email} (v${status.pluginVersion})`;

        const existing = await this.client.getFile(filepath);
        if (existing && existing.content) {
            try {
                const prev = JSON.parse(GiteaClient.fromBase64(existing.content));
                const unchanged = prev.name === status.name
                    && prev.department === status.department
                    && prev.pluginVersion === status.pluginVersion
                    && prev.tbVersion === status.tbVersion;
                if (unchanged) {
                    return { success: true, skipped: 'unchanged' };
                }
            } catch (_) { /* kaputter Vorgänger → einfach überschreiben */ }
            await this.client.updateFile(filepath, body, existing.sha, commitMsg);
        } else {
            await this.client.createFile(filepath, body, commitMsg);
        }
        return { success: true, status };
    }

    async autoDetect() {
        if (!this.isConfigured) return null;
        return await this.client.getConfig();
    }

    async getSyncState() {
        const result = await browser.storage.local.get(SYNC_STATE_KEY);
        return result[SYNC_STATE_KEY] || { fileShas: {} };
    }

    async saveSyncState(state) {
        await browser.storage.local.set({ [SYNC_STATE_KEY]: state });
    }

    async getLocalTemplates() {
        const result = await browser.storage.local.get(TEMPLATE_STORAGE_KEY);
        return result[TEMPLATE_STORAGE_KEY] || [];
    }

    async saveLocalTemplates(templates) {
        await browser.storage.local.set({ [TEMPLATE_STORAGE_KEY]: templates });
    }

    static toFilename(name) {
        return name
            .replace(/[äÄ]/g, 'ae')
            .replace(/[öÖ]/g, 'oe')
            .replace(/[üÜ]/g, 'ue')
            .replace(/ß/g, 'ss')
            .replace(/[/\\:*?"<>|]/g, '-')
            .replace(/^[\s.-]+|[\s.-]+$/g, '')
            .trim();
    }

    /**
     * List all top-level directories in the repo (= available departments)
     */
    async listDepartments() {
        if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');

        const entries = await this.client.listDir('');
        const departments = [];
        for (const entry of entries) {
            if (entry.type === 'dir' && entry.name !== SHARED_FOLDER && entry.name !== USER_FOLDER && entry.name !== CONFIG_FOLDER && entry.name !== CLIENTS_FOLDER && entry.name !== 'signatures' && !entry.name.startsWith('.')) {
                departments.push(entry.name);
            }
        }
        return { success: true, departments };
    }

    /**
     * Lightweight check: get remote file SHAs without downloading content
     */
    async checkRemoteShas() {
        if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');

        const remoteShas = {}; // "folder/filename" -> sha
        const folders = [SHARED_FOLDER];
        if (this.department) folders.push(this.department);
        if (this.authorEmail) folders.push(`${USER_FOLDER}/${this.authorEmail}`);
        // Signatur-Bausteine mitüberwachen, damit Footer-/Header-Änderungen
        // auf dem Server den Hintergrund-Sync (pullSignatures) auslösen.
        folders.push('signatures/footers', 'signatures/headers');

        for (const folder of folders) {
            const files = await this.client.listDir(folder);
            for (const file of files) {
                if (file.name.endsWith('.html')) {
                    remoteShas[`${folder}/${file.name}`] = file.sha;
                }
            }
        }

        return { success: true, remoteShas };
    }

    /**
     * Pull templates from repo: department folder + shared folder
     */
    async pullTemplates() {
        if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');

        const syncState = await this.getSyncState();
        const newTemplates = [];
        const newShas = {};
        let updated = 0;

        // Load from shared + department + personal folders
        const folders = [SHARED_FOLDER];
        if (this.department) folders.push(this.department);
        if (this.authorEmail) folders.push(`${USER_FOLDER}/${this.authorEmail}`);

        for (const folder of folders) {
            const files = await this.client.listDir(folder);
            for (const file of files) {
                if (!file.name.endsWith('.html')) continue;

                const fileData = await this.client.getFile(file.path);
                if (!fileData) continue;

                const content = GiteaClient.fromBase64(fileData.content);
                const templateName = file.name.replace('.html', '');

                newTemplates.push({
                    id: `${folder}/${file.name}`,
                    name: templateName,
                    content: content,
                    folder: folder,
                    remotePath: file.path
                });

                newShas[file.path] = fileData.sha;
                updated++;
            }
        }

        // Merge: keep local-only templates that aren't now on the server
        const existingTemplates = await this.getLocalTemplates();
        const pulledFilenames = new Set(newTemplates.map(t => SyncManager.toFilename(t.name).toLowerCase()));
        const localOnly = existingTemplates.filter(t => !t.remotePath && !pulledFilenames.has(SyncManager.toFilename(t.name).toLowerCase()));

        const merged = [...newTemplates, ...localOnly];
        await this.saveLocalTemplates(merged);
        syncState.fileShas = newShas;
        await this.saveSyncState(syncState);

        return { success: true, updated };
    }

    /**
     * Push local templates back to repo (explicit, with commit author)
     */
    async pushTemplates() {
        if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');
        if (!this.config.authorName) throw new Error('Bitte Name eintragen (für Commit-Zuordnung)');

        const templates = await this.getLocalTemplates();
        const syncState = await this.getSyncState();

        // Group templates by folder
        const byFolder = {};
        for (const t of templates) {
            let folder;
            if (t.folder === SHARED_FOLDER) folder = SHARED_FOLDER;
            else if (t.folder?.startsWith(USER_FOLDER + '/')) folder = t.folder;
            else folder = t.folder || this.department;
            if (!folder) continue; // skip if no department and no explicit folder
            if (!byFolder[folder]) byFolder[folder] = [];
            byFolder[folder].push(t);
        }

        let pushed = 0;

        // Allowed folders: shared + department + personal
        const allowedFolders = [SHARED_FOLDER];
        if (this.department) allowedFolders.push(this.department);
        if (this.authorEmail) allowedFolders.push(`${USER_FOLDER}/${this.authorEmail}`);

        for (const folder of allowedFolders) {
            const localInFolder = byFolder[folder] || [];
            const remoteFiles = await this.client.listDir(folder);
            const remoteByName = {};
            for (const rf of remoteFiles) {
                if (rf.name.endsWith('.html')) {
                    remoteByName[rf.name] = rf;
                }
            }

            // Push each local template
            for (const template of localInFolder) {
                const filename = (SyncManager.toFilename(template.name) || template.id) + '.html';
                const filepath = `${folder}/${filename}`;
                const commitMsg = `${template.name} - bearbeitet von ${this.config.authorName}`;

                const existing = await this.client.getFile(filepath);

                if (existing) {
                    const existingContent = GiteaClient.fromBase64(existing.content);
                    if (existingContent !== template.content) {
                        await this.client.updateFile(filepath, template.content, existing.sha, commitMsg);
                        pushed++;
                    }
                } else {
                    await this.client.createFile(filepath, template.content, commitMsg);
                    pushed++;
                }
            }

            // Delete remote files that were removed locally
            const localNames = new Set(localInFolder.map(t =>
                (SyncManager.toFilename(t.name) || t.id) + '.html'
            ));
            for (const [name, rf] of Object.entries(remoteByName)) {
                if (!localNames.has(name)) {
                    const fileData = await this.client.getFile(rf.path || `${folder}/${name}`);
                    if (fileData) {
                        const commitMsg = `${name} gelöscht von ${this.config.authorName}`;
                        await this.client.deleteFile(fileData.path || `${folder}/${name}`, fileData.sha, commitMsg);
                        pushed++;
                    }
                }
            }
        }

        // Re-pull to get fresh SHAs
        await this.pullTemplates();

        return { success: true, pushed };
    }

    // Kanonischer Signatur-Slug, identisch zum Web-Editor (slugifyHeaderName):
    // toFilename(...) + lowercase + Leerzeichen→Bindestrich. So lesen/schreiben
    // Web-Editor und Plugin DIESELBE Datei (z.B. ".kendrick-bollens.html").
    get sigSlug() {
        return SyncManager.toFilename(this.config.authorName || '')
            .toLowerCase()
            .replace(/\s+/g, '-');
    }

    // Altes Plugin-Format (Groß-/Kleinschreibung + Leerzeichen, z.B.
    // ".Kendrick Bollens.html"). Nur noch für Lese-Fallback, damit Dateien von
    // Clients mit alter Plugin-Version weiterhin angewandt werden. Nicht mehr
    // schreiben — sonst entstehen wieder Doppel-Dateien.
    get authorSlug() {
        return SyncManager.toFilename(this.config.authorName || '');
    }

    static get FOOTER_SEPARATOR() {
        return '<!-- SIG_FOOTER_START -->';
    }

    static extractHeader(fullSignature) {
        const idx = fullSignature.indexOf(SyncManager.FOOTER_SEPARATOR);
        if (idx === -1) return fullSignature;
        return fullSignature.substring(0, idx).trim();
    }

    static combineSignature(header, footer) {
        if (!footer) return header;
        return header + '\n' + SyncManager.FOOTER_SEPARATOR + '\n' + footer;
    }

    /**
     * Load the signature header template from signatures/headers/_vorlage.html
     */
    async loadSignatureTemplate() {
        if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');

        const fileData = await this.client.getFile('signatures/headers/_vorlage.html');
        if (!fileData || !fileData.content) {
            throw new Error('Vorlage nicht gefunden unter signatures/headers/_vorlage.html');
        }

        const html = GiteaClient.fromBase64(fileData.content);
        return { success: true, html };
    }

    /**
     * Pull footer for current department.
     * Tries signatures/footers/{department}.html first, falls back to signatures/footers/_default.html
     */
    async pullFooter() {
        let fileData = null;

        // Try department-specific footer first
        if (this.department) {
            fileData = await this.client.getFile(`signatures/footers/${this.department}.html`);
        }

        // Fall back to default
        if (!fileData || !fileData.content) {
            fileData = await this.client.getFile('signatures/footers/_default.html');
        }

        // Legacy fallback: old single-file location
        if (!fileData || !fileData.content) {
            fileData = await this.client.getFile('signatures/_footer.html');
        }

        if (!fileData || !fileData.content) return '';

        const footer = GiteaClient.fromBase64(fileData.content);
        await browser.storage.local.set({ 'sig_footer_cache': footer });
        return footer;
    }

    /**
     * Resolve the footer file path for a given scope.
     * - 'shared'     → signatures/footers/_default.html (gilt für alle Abteilungen)
     * - 'department' → signatures/footers/{department}.html
     */
    footerPathForScope(scope) {
        if (scope === 'shared') return 'signatures/footers/_default.html';
        if (!this.department) throw new Error('Keine Abteilung ausgewählt');
        return `signatures/footers/${this.department}.html`;
    }

    /**
     * Load footer for editing (returns HTML).
     * - scope 'shared'/'department': lädt genau diese Datei (kein Fallback,
     *   damit der Editor den echten Inhalt der gewählten Ebene zeigt).
     * - ohne scope: Fallback-Kette (Abteilung → gemeinsam) für die
     *   automatische Anwendung an die Signatur.
     */
    async loadFooter(scope) {
        if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');

        if (scope === 'shared' || scope === 'department') {
            const filepath = this.footerPathForScope(scope);
            const fileData = await this.client.getFile(filepath);
            const html = (fileData && fileData.content) ? GiteaClient.fromBase64(fileData.content) : '';
            return { success: true, html, scope };
        }

        const footer = await this.pullFooter();
        return { success: true, html: footer };
    }

    /**
     * Push footer to the chosen scope:
     * - 'shared'     → signatures/footers/_default.html
     * - 'department' → signatures/footers/{department}.html
     */
    async pushFooter(html, scope) {
        if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');
        if (!this.config.authorName) throw new Error('Bitte Name eintragen');

        const targetScope = (scope === 'shared') ? 'shared' : 'department';
        const filepath = this.footerPathForScope(targetScope);
        const label = targetScope === 'shared' ? 'gemeinsam' : this.department;
        const commitMsg = `Signatur-Footer ${label} - von ${this.config.authorName}`;

        const existing = await this.client.getFile(filepath);

        if (existing && existing.content) {
            const existingContent = GiteaClient.fromBase64(existing.content);
            if (existingContent !== html) {
                await this.client.updateFile(filepath, html, existing.sha, commitMsg);
            }
        } else {
            await this.client.createFile(filepath, html, commitMsg);
        }

        // Refresh cache with the footer that actually applies (Abteilung gewinnt
        // vor gemeinsam) — nicht zwingend das gerade gespeicherte html.
        await this.pullFooter();
        return { success: true, scope: targetScope };
    }

    /**
     * Pull signatures from repo using header/footer baustein model.
     * - Loads shared footer from signatures/_footer.html
     * - Loads personal headers from signatures/headers/email.authorslug.html
     * - Combines header + footer and applies to Thunderbird identity
     * - Resolves "=other@" references in second pass
     */
    async pullSignatures() {
        if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');

        // Pull shared footer
        const footer = await this.pullFooter();

        // List header files
        const headerFiles = await this.client.listDir('signatures/headers');
        const headerMap = {};
        for (const f of headerFiles) {
            if (f.name.endsWith('.html') && !f.name.startsWith('_')) {
                headerMap[f.name.toLowerCase()] = f;
            }
        }

        // Get source map
        const sourceResult = await browser.storage.local.get('sig_source_map');
        const sourceMap = sourceResult.sig_source_map || {};

        const accounts = await browser.accounts.list();
        let updated = 0;
        const loadedHeaders = {}; // email → header html
        const allIdentityList = [];

        // First pass: load headers for "own" identities
        for (const account of accounts) {
            const identities = await browser.identities.list(account.id);
            for (const identity of identities) {
                const email = identity.email.toLowerCase();
                const source = sourceMap[email] || 'own';
                allIdentityList.push({ id: identity.id, email });

                if (source.startsWith('=')) continue;

                // Personal-Header suchen. headerMap-Keys sind lowercase, daher
                // Kandidaten ebenfalls lowercase nachschlagen (behebt den Bug,
                // dass Groß-Namen NIE gefunden wurden → Plugin fiel auf die lokale
                // Signatur zurück und revertierte beim Push die Web-Editor-Änderung).
                // Kandidaten:
                //   • kanonisches Web-Editor-Format (lowercase-bindestrich)
                //   • altes Plugin-Format (Groß/Leerzeichen) von Clients mit alter Version
                // Bei mehreren Treffern gewinnt der NEUESTE Commit — so verdeckt eine
                // veraltete Datei keine frische Web-Editor-Änderung, egal welche der
                // beiden Dateien zuletzt im Web-Editor bearbeitet wurde.
                const candidates = [];
                if (this.sigSlug) {
                    const f = headerMap[`${email}.${this.sigSlug}.html`.toLowerCase()];
                    if (f) candidates.push(f);
                }
                if (this.authorSlug) {
                    const f = headerMap[`${email}.${this.authorSlug}.html`.toLowerCase()];
                    if (f && !candidates.includes(f)) candidates.push(f);
                }
                let targetFile = null;
                let targetTime = -1;
                for (const f of candidates) {
                    const t = Date.parse(f.last_author_date || f.last_committer_date || '') || 0;
                    if (t >= targetTime) { targetFile = f; targetTime = t; }
                }

                let header = null;
                if (targetFile) {
                    const fileData = await this.client.getFile(targetFile.path);
                    if (fileData) header = GiteaClient.fromBase64(fileData.content);
                }

                // Fallback: kein Server-Header → lokalen Header aus der aktuellen
                // Signatur verwenden, damit der Footer trotzdem angewandt wird.
                if (header === null) {
                    const existing = identity.signature || '';
                    const localHeader = SyncManager.extractHeader(existing);
                    // Leere, nie eingerichtete Konten nicht mit reinem Footer versehen
                    if (!localHeader.trim() && !existing.includes(SyncManager.FOOTER_SEPARATOR)) continue;
                    header = localHeader;
                }

                loadedHeaders[email] = header;
                const fullSig = SyncManager.combineSignature(header, footer);

                // Nur schreiben, wenn sich etwas ändert (vermeidet unnötige Writes
                // bei jedem Hintergrund-Sync)
                if (fullSig === identity.signature) continue;

                await browser.identities.update(identity.id, {
                    signature: fullSig,
                    signatureIsPlainText: false,
                });
                updated++;
            }
        }

        // Second pass: resolve "=other@" references
        for (const { id, email } of allIdentityList) {
            const source = sourceMap[email] || 'own';
            if (!source.startsWith('=')) continue;

            const srcEmail = source.substring(1).toLowerCase();
            const srcHeader = loadedHeaders[srcEmail];
            if (srcHeader !== undefined) {
                const fullSig = SyncManager.combineSignature(srcHeader, footer);
                await browser.identities.update(id, {
                    signature: fullSig,
                    signatureIsPlainText: false,
                });
                loadedHeaders[email] = srcHeader;
                updated++;
            }
        }

        return { success: true, updated };
    }

    /**
     * Push signature headers to repo.
     * Only the header part is pushed as signatures/headers/email.authorslug.html
     * Skips "=other@" identities.
     */
    async pushSignatures() {
        if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');
        if (!this.config.authorName) throw new Error('Bitte Name eintragen (für Commit-Zuordnung)');
        if (!this.sigSlug) throw new Error('Name fehlt für Dateiname');

        const sourceResult = await browser.storage.local.get('sig_source_map');
        const sourceMap = sourceResult.sig_source_map || {};

        const accounts = await browser.accounts.list();
        let pushed = 0;

        for (const account of accounts) {
            const identities = await browser.identities.list(account.id);
            for (const identity of identities) {
                if (!identity.signature) continue;

                const email = identity.email.toLowerCase();
                const source = sourceMap[email] || 'own';
                if (source.startsWith('=')) continue;

                // Extract just the header from the full signature
                const header = SyncManager.extractHeader(identity.signature);
                if (!header.trim()) continue;

                // Ins kanonische Web-Editor-Format schreiben (lowercase-bindestrich),
                // damit Plugin und Web-Editor dieselbe Datei pflegen. Alte
                // Groß-Dateien bleiben unangetastet (keine Breaking Changes für
                // Clients mit alter Plugin-Version), werden aber nicht mehr neu
                // geschrieben.
                const filename = `${email}.${this.sigSlug}.html`;
                const filepath = `signatures/headers/${filename}`;
                const commitMsg = `Signatur-Header ${identity.email} - von ${this.config.authorName}`;

                const existing = await this.client.getFile(filepath);

                if (existing) {
                    const existingContent = GiteaClient.fromBase64(existing.content);
                    if (existingContent !== header) {
                        await this.client.updateFile(filepath, header, existing.sha, commitMsg);
                        pushed++;
                    }
                } else {
                    await this.client.createFile(filepath, header, commitMsg);
                    pushed++;
                }
            }
        }

        return { success: true, pushed };
    }

    /**
     * Pull a single template by its remote path
     */
    async pullSingleTemplate(remotePath) {
        if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');

        const fileData = await this.client.getFile(remotePath);
        if (!fileData) throw new Error('Datei nicht im Repository gefunden');

        const content = GiteaClient.fromBase64(fileData.content);
        return { success: true, content };
    }

    /**
     * Push a single template by ID
     */
    async pushSingleTemplate(templateId) {
        if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');
        if (!this.config.authorName) throw new Error('Bitte Name eintragen (für Commit-Zuordnung)');

        const templates = await this.getLocalTemplates();
        const template = templates.find(t => t.id === templateId);
        if (!template) throw new Error('Vorlage nicht gefunden');

        const folder = template.folder || this.department;
        if (!folder) throw new Error('Keine Abteilung ausgewählt');

        const filename = (SyncManager.toFilename(template.name) || template.id) + '.html';
        const filepath = `${folder}/${filename}`;
        const commitMsg = `${template.name} - bearbeitet von ${this.config.authorName}`;

        const existing = await this.client.getFile(filepath);

        if (existing) {
            const existingContent = GiteaClient.fromBase64(existing.content);
            if (existingContent !== template.content) {
                await this.client.updateFile(filepath, template.content, existing.sha, commitMsg);
            }
        } else {
            await this.client.createFile(filepath, template.content, commitMsg);
        }

        return { success: true };
    }

    /**
     * Delete a template from the remote repo by its remote path
     */
    async deleteRemoteTemplate(remotePath) {
        if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');
        if (!this.config.authorName) throw new Error('Bitte Name eintragen (für Commit-Zuordnung)');

        const fileData = await this.client.getFile(remotePath);
        if (!fileData) throw new Error('Datei nicht im Repository gefunden');

        const commitMsg = `${remotePath.split('/').pop()} gelöscht von ${this.config.authorName}`;
        await this.client.deleteFile(remotePath, fileData.sha, commitMsg);

        return { success: true };
    }

    static hslToHex(h, s, l) {
        s /= 100; l /= 100;
        const a = s * Math.min(l, 1 - l);
        const f = n => { const k = (n + h / 30) % 12; return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); };
        const toHex = x => Math.round(x * 255).toString(16).padStart(2, '0');
        return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`;
    }

    /**
     * Sync Schlagwörter (tags) from Gitea config to Thunderbird.
     * Auto-registers the current user if not yet listed.
     * Tags are never removed so they remain traceable forever.
     * Format: [ {"name": "Kenny", "color": "#e74c3c"}, ... ]
     */
    async syncTags() {
        if (!this.isConfigured) return { success: true, created: 0, updated: 0 };

        const userName = this.config.authorName;

        // Pull current file (may be null if it doesn't exist yet)
        const fileData = await this.client.getFile(`${CONFIG_FOLDER}/schlagwoerter.json`);
        let schlagwoerter = [];
        let fileSha = null;
        if (fileData) {
            fileSha = fileData.sha;
            try { schlagwoerter = JSON.parse(GiteaClient.fromBase64(fileData.content)); } catch (_) {}
        }
        if (!Array.isArray(schlagwoerter)) schlagwoerter = [];

        // Auto-register current user if not yet listed
        let pushed = false;
        if (userName) {
            const exists = schlagwoerter.some(u => u.name.toLowerCase() === userName.toLowerCase());
            if (!exists) {
                const hue = Math.abs([...userName].reduce((h, c) => ((h << 5) - h + c.charCodeAt(0)) | 0, 0)) % 360;
                const color = SyncManager.hslToHex(hue, 65, 45);
                schlagwoerter.push({ name: userName, color });

                const json = JSON.stringify(schlagwoerter, null, 2);
                const commitMsg = `Schlagwort für ${userName} hinzugefügt`;
                if (fileSha) {
                    await this.client.updateFile(`${CONFIG_FOLDER}/schlagwoerter.json`, json, fileSha, commitMsg);
                } else {
                    await this.client.createFile(`${CONFIG_FOLDER}/schlagwoerter.json`, json, commitMsg);
                }
                pushed = true;
                console.log(`[Sync] Benutzer "${userName}" als Schlagwort registriert`);
            }
        }

        // Cache for background.js access
        await browser.storage.local.set({ [SCHLAGWOERTER_CACHE_KEY]: schlagwoerter });

        // Create/update Thunderbird tags
        const existingTags = await messenger.messages.tags.list();
        const existingByKey = {};
        for (const t of existingTags) existingByKey[t.key] = t;

        let created = 0, updated = 0;

        for (const user of schlagwoerter) {
            const key = `$hps_${user.name.toLowerCase().replace(/\s+/g, '_')}`;
            const color = user.color || '#999999';

            if (existingByKey[key]) {
                if (existingByKey[key].color !== color) {
                    await messenger.messages.tags.update(key, { color });
                    updated++;
                }
            } else {
                await messenger.messages.tags.create(key, user.name, color);
                created++;
            }
        }

        return { success: true, created, updated, pushed };
    }

    /**
     * Get the current user's tag key based on config
     */
    getMyTagKey(schlagwoerter) {
        if (!this.config || !Array.isArray(schlagwoerter)) return null;
        const name = this.config.authorName;
        if (!name) return null;

        const match = schlagwoerter.find(u => u.name.toLowerCase() === name.toLowerCase());
        if (!match) return null;

        return `$hps_${match.name.toLowerCase().replace(/\s+/g, '_')}`;
    }

    async testConnection() {
        if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');
        const repoInfo = await this.client.testConnection();
        return { success: true, repoName: repoInfo.full_name };
    }
}

// ── Global sync manager instance ──

const syncManager = new SyncManager();

// ── Background message handler for sync ──

browser.runtime.onMessage.addListener(async (msg, sender) => {
    const syncActions = ['testConnection', 'pullTemplates', 'pushTemplates', 'pullSingleTemplate', 'pushSingleTemplate', 'deleteRemoteTemplate', 'listDepartments', 'addDepartment', 'checkRemoteShas', 'pullSignatures', 'pushSignatures', 'loadSignatureTemplate', 'loadFooter', 'pushFooter', 'autoDetect', 'syncTags', 'getMyTagKey'];
    if (!syncActions.includes(msg.action)) return;

    try {
        const initialized = await syncManager.init();
        if (!initialized && msg.action !== 'testConnection') {
            return { success: false, error: 'Sync nicht konfiguriert. Bitte Verbindung einrichten.' };
        }

        switch (msg.action) {
            case 'testConnection':
                // Init with provided config for testing before save
                if (!initialized) return { success: false, error: 'Sync nicht konfiguriert.' };
                return await syncManager.testConnection();

            case 'listDepartments':
                return await syncManager.listDepartments();

            case 'addDepartment': {
                if (!syncManager.isConfigured) throw new Error('Sync nicht konfiguriert');
                const name = msg.name?.trim();
                if (!name) throw new Error('Kein Name angegeben');
                await syncManager.client.createFile(`${name}/.gitkeep`, '', `Abteilung "${name}" angelegt`);
                return { success: true };
            }

            case 'checkRemoteShas':
                return await syncManager.checkRemoteShas();

            case 'pullTemplates':
                return await syncManager.pullTemplates();

            case 'pushTemplates':
                return await syncManager.pushTemplates();

            case 'pullSingleTemplate':
                return await syncManager.pullSingleTemplate(msg.remotePath);

            case 'pushSingleTemplate':
                return await syncManager.pushSingleTemplate(msg.templateId);

            case 'deleteRemoteTemplate':
                return await syncManager.deleteRemoteTemplate(msg.remotePath);

            case 'pullSignatures':
                return await syncManager.pullSignatures();

            case 'pushSignatures':
                return await syncManager.pushSignatures();

            case 'loadSignatureTemplate':
                return await syncManager.loadSignatureTemplate();

            case 'loadFooter':
                return await syncManager.loadFooter(msg.scope);

            case 'pushFooter':
                return await syncManager.pushFooter(msg.html, msg.scope);

            case 'autoDetect':
                const config = await syncManager.autoDetect();
                return { success: true, config };

            case 'syncTags':
                return await syncManager.syncTags();

            case 'getMyTagKey': {
                const cached = (await browser.storage.local.get(SCHLAGWOERTER_CACHE_KEY))[SCHLAGWOERTER_CACHE_KEY];
                const tagKey = syncManager.getMyTagKey(cached);
                return { success: !!tagKey, tagKey };
            }

            default:
                return { success: false, error: 'Unbekannte Aktion' };
        }
    } catch (err) {
        console.error('Sync error:', err);
        return { success: false, error: err.message };
    }
});

// ── Auto-sync on startup (pull only) ──

let lastKnownShas = null;
let lastFullSync = 0;
let lastTagSync = 0;
let syncInProgress = false;
let clientStatusReported = false;
const TAG_SYNC_INTERVAL_MS = 60 * 1000;
const HASH_STORAGE_KEY_BG = 'sync_hashes';

function simpleHashBg(str) {
    let h = 0;
    for (let i = 0; i < str.length; i++) {
        h = ((h << 5) - h + str.charCodeAt(i)) | 0;
    }
    return h;
}

async function updateSyncHashes() {
    const templates = await syncManager.getLocalTemplates();
    const result = await browser.storage.local.get(HASH_STORAGE_KEY_BG);
    const data = result[HASH_STORAGE_KEY_BG] || {};
    data.tpl = {};
    for (const t of templates) {
        data.tpl[t.id] = simpleHashBg(t.content || '');
    }

    // Also update signature hashes from Thunderbird identities
    try {
        const accounts = await browser.accounts.list();
        data.sig = data.sig || {};
        for (const account of accounts) {
            const identities = await browser.identities.list(account.id);
            for (const identity of identities) {
                data.sig[identity.email.toLowerCase()] = simpleHashBg(identity.signature || '');
            }
        }
    } catch (_) {}

    await browser.storage.local.set({ [HASH_STORAGE_KEY_BG]: data });
}

async function smartSync() {
    if (syncInProgress) return;
    try {
        const initialized = await syncManager.init();
        if (!initialized) return;

        // Einmal pro Session den eigenen Client-Status melden (Admin-Übersicht).
        // Fire-and-forget, blockiert den Sync nicht.
        if (!clientStatusReported) {
            clientStatusReported = true;
            syncManager.reportClientStatus()
                .then((r) => { if (r?.status) console.log('[Sync] Client-Status gemeldet:', r.status.email, 'v' + r.status.pluginVersion); })
                .catch((e) => console.error('[Sync] Client-Status fehlgeschlagen:', e));
        }

        syncInProgress = true;

        // Tag sync every 60s (schlagwoerter.json is not in SHA-checked folders)
        const now = Date.now();
        if (now - lastTagSync >= TAG_SYNC_INTERVAL_MS) {
            lastTagSync = now;
            try {
                const tagResult = await syncManager.syncTags();
                if (tagResult.created || tagResult.pushed) console.log('[Sync] Tags synchronisiert:', tagResult);
            } catch (e) {
                console.error('[Sync] Tag-Sync fehlgeschlagen:', e);
            }
        }

        // Lightweight SHA check for templates + signatures
        const shaResult = await syncManager.checkRemoteShas();
        if (!shaResult?.success) { syncInProgress = false; return; }

        const currentShas = JSON.stringify(shaResult.remoteShas);

        // First run or SHAs changed → full pull
        if (lastKnownShas === null || currentShas !== lastKnownShas) {
            if (now - lastFullSync < FULL_SYNC_COOLDOWN_MS) { syncInProgress = false; return; }

            lastFullSync = now;
            console.log('[Sync] Änderung erkannt, lade Vorlagen...');

            const result = await syncManager.pullTemplates();
            console.log('[Sync] Vorlagen geladen:', result);
            await updateSyncHashes();

            const sigResult = await syncManager.pullSignatures();
            console.log('[Sync] Signaturen geladen:', sigResult);

            lastKnownShas = JSON.stringify((await syncManager.checkRemoteShas()).remoteShas || {});
        }
        syncInProgress = false;
    } catch (err) {
        console.error('[Sync] Check fehlgeschlagen:', err);
        syncInProgress = false;
    }
}

smartSync();
setInterval(smartSync, SHA_CHECK_INTERVAL_MS);

