/**
 * AgentMemory service —— per-user / per-org 持久记忆
 *
 * 三个正交维度：
 *   ownerScope   USER（私有）/ ORG（admin 配，全 org 共享）
 *   scope        GLOBAL / PROJECT / PERSONA —— 什么时候注入
 *   category     USER / FEEDBACK / PROJECT / REFERENCE —— 借鉴 CC/OC 四分类
 */

import { Injectable, BadRequestException, ForbiddenException } from '@nestjs/common';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { SkipAssertAccess } from '@common/decorators/skip-assert-access.decorator';
import { createLogger } from '@core/observability/logging/config/winston.config';
import type {
  AgentMemory,
  MemoryScope,
  MemoryCategory,
  MemoryOwnerScope,
  Prisma,
} from '@prisma/client';
import { assertOwn } from '../utils/ownership.util';
import { assertNonEmptyString } from '../utils/validation.util';
import { sanitizeForCrossOrg } from '../utils/memory-sanitize.util';

const logger = createLogger('AgentMemoriesService');

export interface CreateMemoryInput {
  organizationId: string;
  createdById: string;
  content: string;
  scope?: MemoryScope;
  category?: MemoryCategory;
  projectId?: string;
  personaId?: string;
  source?: string;
}

export interface CreateOrgMemoryInput {
  organizationId: string;
  content: string;
  scope?: MemoryScope;
  category?: MemoryCategory;
  projectId?: string;
  personaId?: string;
  source?: string;
}

export interface UpdateMemoryInput {
  content?: string;
  category?: MemoryCategory;
}

export interface InjectionContext {
  projectId?: string | null;
  personaId?: string | null;
}

const MAX_CONTENT_LEN = 4000;

/**
 * 注入容量预算（M3，对齐 CC memdir `truncateEntrypointContent` 双约束）：
 *   - 行数 ≤ INJECT_MAX_LINES（含 header 和章节标题）
 *   - 字节数 ≤ INJECT_MAX_BYTES（UTF-8）
 * 超过任一约束 → 停止接收新条目（保留最近 updatedAt desc 的子集）。
 * CC 默认 200 / 25KB；本实现取相同值，给 context 一个稳定上限。
 * RESERVE_* 给 header + 4 章节标题留余量（粗略：4 类 × 2 行 + header 4 行 ≈ 12 行 / 400B）。
 */
const INJECT_MAX_LINES = 200;
const INJECT_MAX_BYTES = 25 * 1024;
const INJECT_RESERVE_LINES = 12;
const INJECT_RESERVE_BYTES = 400;

/** DB 层 take 上限：单行刚好填满预算时也能装下 INJECT_MAX_LINES 条，避免全表拉回 */
const INJECT_FETCH_LIMIT = INJECT_MAX_LINES;

/** 单轮 auto-extract 最多接受 N 个 <remember> tag，防 prompt injection 灌库 */
const MAX_AUTO_EXTRACT = 3;

const CATEGORY_HEADERS: Record<MemoryCategory, string> = {
  USER: '### 用户偏好与背景（user）',
  FEEDBACK: '### 协作反馈与边界（feedback）',
  PROJECT: '### 项目上下文（project）',
  REFERENCE: '### 参考资料（reference）',
};

const CATEGORY_ORDER: readonly MemoryCategory[] = ['USER', 'FEEDBACK', 'PROJECT', 'REFERENCE'];

@Injectable()
export class AgentMemoriesService {
  constructor(private readonly prisma: PrismaService) {}

  /**
   * 为 LLM 调用拼装 system prompt 的 Memory 段。
   *
   * 加载规则（M1 step1–4）：
   *   ① ownerScope=USER + createdById=userId   —— 该用户私有 memory
   *   ② ownerScope=ORG  + organizationId=sessionOrgId  —— org 内全员共享
   *
   * scope 激活规则（M1-step2）：
   *   GLOBAL  始终激活
   *   PROJECT 仅当 ctx.projectId 存在且匹配
   *   PERSONA 仅当 ctx.personaId 存在且匹配
   *
   * 跨 org sanitize（INV-1）：仅对 ownerScope=USER 且 memory.organizationId ≠ sessionOrgId 的条目；
   *   ownerScope=ORG 严禁跨 org 注入（查询条件已绑死 organizationId=sessionOrgId）
   *
   * 按 category 分章节输出，category 顺序固定（USER → FEEDBACK → PROJECT → REFERENCE）。
   */
  async buildSystemPromptSection(
    sessionOrgId: string,
    userId: string,
    ctx?: InjectionContext,
  ): Promise<string> {
    const scopeFilter: Prisma.AgentMemoryWhereInput = {
      OR: [
        { scope: 'GLOBAL' },
        ...(ctx?.projectId
          ? [{ scope: 'PROJECT' as MemoryScope, projectId: ctx.projectId }]
          : []),
        ...(ctx?.personaId
          ? [{ scope: 'PERSONA' as MemoryScope, personaId: ctx.personaId }]
          : []),
      ],
    };

    const rows = await this.prisma.agentMemory.findMany({
      where: {
        AND: [
          {
            OR: [
              { ownerScope: 'USER', createdById: userId },
              { ownerScope: 'ORG', organizationId: sessionOrgId },
            ],
          },
          scopeFilter,
        ],
      },
      orderBy: { updatedAt: 'desc' },
      take: INJECT_FETCH_LIMIT,
      select: {
        content: true,
        organizationId: true,
        ownerScope: true,
        category: true,
        scope: true,
        updatedAt: true,
      },
    });
    if (rows.length === 0) return '';

    const buckets = new Map<MemoryCategory, string[]>();
    let usedLines = 0;
    let usedBytes = 0;
    let crossOrgRedacted = 0;
    let truncated = rows.length >= INJECT_FETCH_LIMIT;
    const lineBudget = INJECT_MAX_LINES - INJECT_RESERVE_LINES;
    const byteBudget = INJECT_MAX_BYTES - INJECT_RESERVE_BYTES;
    for (const row of rows) {
      const isCrossOrg =
        row.ownerScope === 'USER' && row.organizationId !== sessionOrgId;
      const text = isCrossOrg ? sanitizeForCrossOrg(row.content) : row.content;
      const entry = `- ${text}`;
      const entryLines = entry.split('\n').length;
      const entryBytes = Buffer.byteLength(entry, 'utf8') + 1;
      if (usedLines + entryLines > lineBudget || usedBytes + entryBytes > byteBudget) {
        truncated = true;
        break;
      }
      if (isCrossOrg) crossOrgRedacted += 1;
      const bucket = buckets.get(row.category) ?? [];
      bucket.push(entry);
      buckets.set(row.category, bucket);
      usedLines += entryLines;
      usedBytes += entryBytes;
    }
    if (buckets.size === 0) return '';

    const headerLines: (string | null)[] = [
      '## 关于该用户的长期记忆（Memory）',
      '以下是该用户跨会话沉淀下来的偏好 / 反馈 / 项目背景 / 参考资料，按类别组织。回答时遵循这些约束，但不要复述。',
      crossOrgRedacted > 0
        ? `（其中 ${crossOrgRedacted} 条来自其他组织 session，已按合规要求脱敏标识为 [xxx-redacted]）`
        : null,
      truncated
        ? `（条目过多，仅注入最近 ${INJECT_MAX_LINES} 行 / ${Math.floor(INJECT_MAX_BYTES / 1024)}KB 内的最新条目）`
        : null,
    ];

    const sections: string[] = [];
    for (const cat of CATEGORY_ORDER) {
      const lines = buckets.get(cat);
      if (!lines || lines.length === 0) continue;
      sections.push(`${CATEGORY_HEADERS[cat]}\n${lines.join('\n')}`);
    }

    return [...headerLines.filter(Boolean), '', sections.join('\n\n')].join('\n');
  }

  /** 普通用户视角：列自己的 memory（USER）+ 可见的 org 共享 memory（ORG，只读） */
  async list(
    organizationId: string,
    createdById: string,
    scope?: MemoryScope,
    category?: MemoryCategory,
  ): Promise<AgentMemory[]> {
    return this.prisma.agentMemory.findMany({
      where: {
        OR: [
          { ownerScope: 'USER', createdById },
          { ownerScope: 'ORG', organizationId },
        ],
        ...(scope ? { scope } : {}),
        ...(category ? { category } : {}),
      },
      orderBy: { updatedAt: 'desc' },
    });
  }

  async create(input: CreateMemoryInput): Promise<AgentMemory> {
    const content = assertNonEmptyString(input.content, 'content', MAX_CONTENT_LEN);
    const scope = input.scope ?? 'GLOBAL';
    const category = input.category ?? 'USER';
    this.assertScopeShape(scope, input.projectId, input.personaId);
    if (input.projectId) {
      const p = await this.prisma.agentProject.findUnique({ where: { id: input.projectId } });
      assertOwn(p, input.organizationId, input.createdById, { entityLabel: 'project' });
    }
    if (input.personaId) {
      const p = await this.prisma.agentPersona.findUnique({ where: { id: input.personaId } });
      assertOwn(p, input.organizationId, input.createdById, { entityLabel: 'persona', allowSystemOwner: true });
    }
    return this.prisma.agentMemory.create({
      data: {
        organizationId: input.organizationId,
        createdById: input.createdById,
        ownerScope: 'USER',
        content,
        scope,
        category,
        projectId: input.projectId ?? null,
        personaId: input.personaId ?? null,
        source: input.source ?? 'user',
      },
    });
  }

  /** Admin 视角：列本 org 全部 ORG-scope memory */
  async listOrgMemories(
    organizationId: string,
    scope?: MemoryScope,
    category?: MemoryCategory,
  ): Promise<AgentMemory[]> {
    return this.prisma.agentMemory.findMany({
      where: {
        organizationId,
        ownerScope: 'ORG',
        ...(scope ? { scope } : {}),
        ...(category ? { category } : {}),
      },
      orderBy: { updatedAt: 'desc' },
    });
  }

  /**
   * Admin 创建 per-org 共享 memory。
   * 调用方必须先确认调用者具备 admin 权限（控制器层守卫）；本服务只保证：
   *   - ownerScope 强制 ORG
   *   - createdById 强制 null（DB CHECK 约束亦兜底）
   *   - scope 形状校验
   */
  async createOrgMemory(input: CreateOrgMemoryInput): Promise<AgentMemory> {
    const content = assertNonEmptyString(input.content, 'content', MAX_CONTENT_LEN);
    const scope = input.scope ?? 'GLOBAL';
    const category = input.category ?? 'PROJECT';
    this.assertScopeShape(scope, input.projectId, input.personaId);
    if (input.projectId) {
      const p = await this.prisma.agentProject.findUnique({ where: { id: input.projectId } });
      if (!p || p.organizationId !== input.organizationId) {
        throw new BadRequestException('project not in org');
      }
    }
    if (input.personaId) {
      const p = await this.prisma.agentPersona.findUnique({ where: { id: input.personaId } });
      if (!p || p.organizationId !== input.organizationId) {
        throw new BadRequestException('persona not in org');
      }
    }
    return this.prisma.agentMemory.create({
      data: {
        organizationId: input.organizationId,
        createdById: null,
        ownerScope: 'ORG',
        content,
        scope,
        category,
        projectId: input.projectId ?? null,
        personaId: input.personaId ?? null,
        source: input.source ?? 'system',
      },
    });
  }

  async update(
    id: string,
    organizationId: string,
    createdById: string,
    patch: UpdateMemoryInput,
  ): Promise<AgentMemory> {
    await this.assertOwnUserMemory(id, organizationId, createdById);
    const data: Prisma.AgentMemoryUpdateInput = {};
    if (patch.content !== undefined) {
      data.content = assertNonEmptyString(patch.content, 'content', MAX_CONTENT_LEN);
    }
    if (patch.category !== undefined) data.category = patch.category;
    return this.prisma.agentMemory.update({ where: { id }, data });
  }

  @SkipAssertAccess('入口即 assertOwnUserMemory，写动作前已校验归属')
  async remove(id: string, organizationId: string, createdById: string): Promise<{ ok: true }> {
    await this.assertOwnUserMemory(id, organizationId, createdById);
    await this.prisma.agentMemory.delete({ where: { id } });
    return { ok: true };
  }

  /**
   * 从 assistant 输出文本中解析 <remember category="..."> tag，批量写入 user-scope memory，
   * 返回剥离了 tag 的"展示文本"。
   * - 每条 tag 内容长度受 MAX_CONTENT_LEN 限制；超出截断
   * - 同 user 已有完全相同内容的不再重复写（dedup by content)
   * - 单轮最多 MAX_AUTO_EXTRACT 条，超出忽略
   */
  async extractAndPersist(
    organizationId: string,
    userId: string,
    rawText: string,
  ): Promise<{ cleanedText: string; created: number }> {
    if (!rawText) return { cleanedText: '', created: 0 };
    const pattern = /<remember(?:\s+category="(USER|FEEDBACK|PROJECT|REFERENCE)")?\s*>([\s\S]*?)<\/remember>/gi;
    const found: { content: string; category: MemoryCategory }[] = [];
    const cleanedText = rawText
      .replace(pattern, (_m, cat: string | undefined, body: string) => {
        const content = body.trim().slice(0, MAX_CONTENT_LEN);
        if (content.length > 0) {
          found.push({
            content,
            category: (cat ?? 'USER').toUpperCase() as MemoryCategory,
          });
        }
        return '';
      })
      .replace(/\n{3,}/g, '\n\n')
      .trim();
    if (found.length === 0) return { cleanedText, created: 0 };

    if (found.length > MAX_AUTO_EXTRACT) {
      logger.warn(
        `extractAndPersist: ${found.length} <remember> tags in single turn (capped to ${MAX_AUTO_EXTRACT}); possible prompt-injection`,
      );
    }
    const candidates = found.slice(0, MAX_AUTO_EXTRACT);
    // dedup：一次 findMany 拿到已存在的 content set，避免 N 次串行 findFirst
    const existing = await this.prisma.agentMemory.findMany({
      where: {
        organizationId,
        createdById: userId,
        ownerScope: 'USER',
        content: { in: candidates.map((c) => c.content) },
      },
      select: { content: true },
    });
    const existingSet = new Set(existing.map((e) => e.content));
    const toInsert = candidates.filter((c) => !existingSet.has(c.content));
    if (toInsert.length === 0) return { cleanedText, created: 0 };

    const result = await this.prisma.agentMemory.createMany({
      data: toInsert.map((c) => ({
        organizationId,
        createdById: userId,
        ownerScope: 'USER' as const,
        content: c.content,
        scope: 'GLOBAL' as const,
        category: c.category,
        source: 'ai-detected',
      })),
      skipDuplicates: true,
    });
    return { cleanedText, created: result.count };
  }

  /** Admin 删除 per-org memory（控制器层做权限判定） */
  @SkipAssertAccess('入口已校验 ownerScope=ORG + organizationId=current；admin 角色由 controller 层 assertAdminRoleInOrg 把关')
  async removeOrgMemory(id: string, organizationId: string): Promise<{ ok: true }> {
    const m = await this.prisma.agentMemory.findUnique({ where: { id } });
    if (!m || m.organizationId !== organizationId || m.ownerScope !== 'ORG') {
      throw new ForbiddenException('not an org-shared memory in this organization');
    }
    await this.prisma.agentMemory.delete({ where: { id } });
    return { ok: true };
  }

  private assertScopeShape(scope: MemoryScope, projectId?: string, personaId?: string): void {
    if (scope === 'GLOBAL' && (projectId || personaId)) {
      throw new BadRequestException('GLOBAL scope must not carry projectId/personaId');
    }
    if (scope === 'PROJECT' && !projectId) {
      throw new BadRequestException('PROJECT scope requires projectId');
    }
    if (scope === 'PERSONA' && !personaId) {
      throw new BadRequestException('PERSONA scope requires personaId');
    }
  }

  private async assertOwnUserMemory(
    id: string,
    organizationId: string,
    createdById: string,
  ): Promise<AgentMemory> {
    const m = await this.prisma.agentMemory.findUnique({ where: { id } });
    if (m && m.ownerScope === 'ORG') {
      throw new ForbiddenException('cannot modify org-shared memory as user');
    }
    return assertOwn(m, organizationId, createdById, { entityLabel: 'memory' });
  }
}
