/**
 * ============================================================
 * Episodic Memory (일화 기억)
 * 
 * 과거 대화/회의/결정의 맥락 흐름을 저장합니다.
 * 세션 종료 시 자동으로 에피소드를 요약하여 저장합니다.
 * "왜 이렇게 결정했는지", "어떤 흐름으로 진행했는지" 기록.
 * 저장 위치: {brainPath}/memory/episodes/*.json
 * ============================================================
 */

import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { EpisodicEntry, MemoryContextResult } from './types';

export class EpisodicMemory {
    private episodeDir: string;
    private maxEpisodes: number;
    /**
     * mtime-keyed cache of the parsed episode list. The previous implementation
     * re-read and re-parsed every episode JSON from disk on every message. We now
     * cache the parsed result and re-read only when the episode directory's mtime
     * changes (a new/removed/rewritten episode bumps the directory mtime). This
     * mirrors the mtime-keyed caching style of `retrieval/brainIndex.ts`.
     */
    private _episodeCache: { dirMtimeMs: number; episodes: EpisodicEntry[] } | null = null;

    constructor(brainPath: string, maxEpisodes = 50) {
        this.episodeDir = path.join(brainPath, 'memory', 'episodes');
        this.maxEpisodes = maxEpisodes;
        if (!fs.existsSync(this.episodeDir)) {
            fs.mkdirSync(this.episodeDir, { recursive: true });
        }
    }

    // ─── Episode Creation ───

    /**
     * 대화 히스토리에서 에피소드를 생성하고 저장합니다.
     * LLM 호출 없이 패턴 기반으로 요약합니다.
     */
    public createEpisode(
        sessionId: string,
        messages: Array<{ role: string; content: string; timestamp?: number }>,
        projectContext?: string
    ): EpisodicEntry | null {
        // 너무 짧은 대화는 에피소드로 저장하지 않음
        const userMessages = messages.filter((m) => m.role === 'user');
        if (userMessages.length < 2) return null;

        const title = this.generateTitle(userMessages);
        const summary = this.generateSummary(messages);
        const keyDecisions = this.extractDecisions(messages);
        const topics = this.extractTopics(messages);

        const firstTimestamp = messages[0]?.timestamp || Date.now();
        const lastTimestamp = messages[messages.length - 1]?.timestamp || Date.now();

        const episode: EpisodicEntry = {
            id: crypto.randomUUID(),
            sessionId,
            title,
            summary,
            keyDecisions,
            topics,
            projectContext,
            timestamp: firstTimestamp,
            duration: lastTimestamp - firstTimestamp,
            messageCount: messages.length
        };

        this.saveEpisode(episode);
        this.pruneOldEpisodes();
        return episode;
    }

    private saveEpisode(episode: EpisodicEntry): void {
        try {
            const date = new Date(episode.timestamp).toISOString().slice(0, 10);
            const slug = episode.title
                .toLowerCase()
                .replace(/[^a-z0-9가-힣]+/g, '_')
                .slice(0, 40);
            const fileName = `ep_${date}_${slug}.json`;
            const filePath = path.join(this.episodeDir, fileName);

            fs.writeFileSync(filePath, JSON.stringify(episode, null, 2), 'utf-8');
        } catch { /* silently fail */ }
    }

    // ─── Episode Retrieval ───

    /**
     * 저장된 모든 에피소드를 최신순으로 로드합니다.
     *
     * Result is cached and re-read only when the episode directory's mtime
     * changes — creating, deleting, or rewriting an episode file all bump the
     * directory mtime, so the cache stays correct without per-message disk reads.
     */
    public loadAllEpisodes(): EpisodicEntry[] {
        try {
            const dirMtimeMs = fs.statSync(this.episodeDir).mtimeMs;
            const cached = this._episodeCache;
            if (cached && cached.dirMtimeMs === dirMtimeMs) {
                return cached.episodes.slice();
            }

            const files = fs.readdirSync(this.episodeDir)
                .filter((f) => f.endsWith('.json'))
                .sort()
                .reverse();

            const episodes: EpisodicEntry[] = [];
            for (const file of files) {
                try {
                    const raw = fs.readFileSync(path.join(this.episodeDir, file), 'utf-8');
                    episodes.push(JSON.parse(raw) as EpisodicEntry);
                } catch { /* skip corrupted */ }
            }

            this._episodeCache = { dirMtimeMs, episodes };
            return episodes.slice();
        } catch {
            return [];
        }
    }

    /**
     * 프롬프트와 관련된 에피소드를 검색합니다.
     */
    public findRelevantEpisodes(prompt: string, limit = 3): EpisodicEntry[] {
        // Temporal + Distillation 필터: 만료된 episode 와 LongTerm 으로 이미 promote 된
        // episode 는 검색에서 제외 (digest 가 LongTerm 에 있으니 중복 노출 방지).
        const now = Date.now();
        const episodes = this.loadAllEpisodes()
            .filter((ep) => !ep.expiresAt || ep.expiresAt > now)
            .filter((ep) => !ep.promoted);
        const promptLower = prompt.toLowerCase();
        const terms = promptLower
            .split(/[^a-z0-9가-힣_]+/g)
            .filter((t) => t.length >= 2)
            .slice(0, 20);

        const scored = episodes.map((ep) => {
            let score = 0;
            const searchText = [ep.title, ep.summary, ...ep.keyDecisions, ...ep.topics]
                .join(' ')
                .toLowerCase();

            for (const term of terms) {
                if (searchText.includes(term)) score += 1;
            }

            // Topic match gets extra weight
            for (const topic of ep.topics) {
                if (promptLower.includes(topic.toLowerCase())) score += 3;
            }

            // Recency boost
            const daysAgo = (Date.now() - ep.timestamp) / (1000 * 60 * 60 * 24);
            if (daysAgo < 3) score += 2;
            else if (daysAgo < 7) score += 1;

            return { episode: ep, score };
        });

        return scored
            .filter((s) => s.score > 0)
            .sort((a, b) => b.score - a.score)
            .slice(0, limit)
            .map((s) => s.episode);
    }

    // ─── Context Building ───

    public buildContext(currentPrompt: string, limit = 3): MemoryContextResult | null {
        const relevant = this.findRelevantEpisodes(currentPrompt, limit);
        if (relevant.length === 0) return null;

        const content = relevant
            .map((ep) => {
                const date = new Date(ep.timestamp).toISOString().slice(0, 10);
                const decisions = ep.keyDecisions.length > 0
                    ? `\n    Decisions: ${ep.keyDecisions.join('; ')}`
                    : '';
                return `- [${date}] ${ep.title}: ${ep.summary}${decisions}`;
            })
            .join('\n');

        return {
            layer: 'episodic',
            label: 'Episodic Memory (과거 대화 / 결정 흐름)',
            content,
            relevance: 0.7
        };
    }

    // ─── Internal Helpers ───

    private generateTitle(userMessages: Array<{ content: string }>): string {
        // 첫 번째 사용자 메시지에서 제목 생성
        const first = userMessages[0]?.content || '';
        const cleaned = first
            .replace(/```[\s\S]*?```/g, '')     // 코드 블록 제거
            .replace(/\n+/g, ' ')
            .trim();

        if (cleaned.length <= 60) return cleaned || 'Untitled Session';
        return cleaned.slice(0, 57) + '...';
    }

    private generateSummary(
        messages: Array<{ role: string; content: string }>
    ): string {
        // 사용자 메시지의 핵심 키워드 기반 요약
        const userMessages = messages
            .filter((m) => m.role === 'user')
            .map((m) => m.content.replace(/```[\s\S]*?```/g, '').trim());

        const allText = userMessages.join(' ');

        if (allText.length <= 200) return allText;

        // Take first and last user messages for summary
        const firstMsg = userMessages[0]?.slice(0, 100) || '';
        const lastMsg = userMessages[userMessages.length - 1]?.slice(0, 100) || '';

        return `시작: ${firstMsg} → 최종: ${lastMsg}`;
    }

    private extractDecisions(
        messages: Array<{ role: string; content: string }>
    ): string[] {
        const decisions: string[] = [];
        const patterns = [
            /(?:결정|decided|결론|확정)[\s:：]+(.{10,120})/gi,
            /(?:으로\s*(?:하자|가자|결정|확정))[.!]?\s*(.{0,80})/g,
            /(?:let's go with|we'll use|confirmed|선택)\s+(.{5,80})/gi
        ];

        for (const msg of messages) {
            for (const pattern of patterns) {
                pattern.lastIndex = 0;
                let match;
                while ((match = pattern.exec(msg.content)) !== null) {
                    const decision = (match[1] || match[0]).trim();
                    if (decision.length > 5 && !decisions.includes(decision)) {
                        decisions.push(decision);
                    }
                }
            }
        }

        return decisions.slice(0, 5);
    }

    private extractTopics(
        messages: Array<{ role: string; content: string }>
    ): string[] {
        const allText = messages
            .filter((m) => m.role === 'user')
            .map((m) => m.content)
            .join(' ')
            .toLowerCase();

        // Extract frequent meaningful terms
        const words = allText
            .split(/[^a-z0-9가-힣_]+/g)
            .filter((w) => w.length >= 3);

        const freq = new Map<string, number>();
        for (const word of words) {
            freq.set(word, (freq.get(word) || 0) + 1);
        }

        return Array.from(freq.entries())
            .filter(([, count]) => count >= 2)
            .sort(([, a], [, b]) => b - a)
            .slice(0, 8)
            .map(([word]) => word);
    }

    /**
     * 단일 episode 의 promoted 플래그를 true 로 마킹하고 LongTerm digest id 를 기록.
     * Distillation Loop 가 호출. 파일 rewrite 1회.
     */
    public markPromoted(episodeId: string, longTermDigestId: string): boolean {
        const episodes = this.loadAllEpisodes();
        const ep = episodes.find((e) => e.id === episodeId);
        if (!ep) return false;
        ep.promoted = true;
        ep.promotedToLongTermId = longTermDigestId;
        // Find the file holding this episode and rewrite.
        try {
            const files = fs.readdirSync(this.episodeDir).filter((f) => f.endsWith('.json'));
            for (const file of files) {
                const full = path.join(this.episodeDir, file);
                try {
                    const raw = fs.readFileSync(full, 'utf-8');
                    const parsed = JSON.parse(raw) as EpisodicEntry;
                    if (parsed.id === episodeId) {
                        parsed.promoted = true;
                        parsed.promotedToLongTermId = longTermDigestId;
                        fs.writeFileSync(full, JSON.stringify(parsed, null, 2), 'utf-8');
                        this._episodeCache = null; // dir mtime bump → cache 다음 호출에 갱신
                        return true;
                    }
                } catch { /* skip */ }
            }
        } catch { /* ignore */ }
        return false;
    }

    /** Distillation 후보 — 지정 일수보다 오래되고 아직 promoted 되지 않은 episodes. */
    public findStaleEpisodes(ageThresholdDays: number): EpisodicEntry[] {
        const cutoff = Date.now() - ageThresholdDays * 24 * 60 * 60 * 1000;
        return this.loadAllEpisodes()
            .filter((ep) => !ep.promoted)
            .filter((ep) => ep.timestamp < cutoff);
    }

    private pruneOldEpisodes(): void {
        try {
            const files = fs.readdirSync(this.episodeDir)
                .filter((f) => f.endsWith('.json'))
                .sort()
                .reverse();

            // Delete episodes beyond the max limit
            if (files.length > this.maxEpisodes) {
                const toDelete = files.slice(this.maxEpisodes);
                for (const file of toDelete) {
                    try {
                        fs.unlinkSync(path.join(this.episodeDir, file));
                    } catch { /* ignore */ }
                }
            }
        } catch { /* ignore */ }
    }
}
