import { HttpStatus, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BusinessException } from '../../common/exceptions/business.exception';
import {
  paginated,
  resolvePage,
  type PaginatedResult,
} from '../../common/pagination/pagination';
import { TaskService } from '../task/task.service';
import type { TaskStatus } from '../task/entities/task.entity';
import { ProjectService } from '../project/project.service';
import { AiConversation } from './entities/ai-conversation.entity';
import { AiMessage } from './entities/ai-message.entity';

// Anthropic Messages API 응답(필요 필드만)
interface AnthropicResponse {
  content?: { type: string; text?: string }[];
}

// 외부 노출 형태
export interface ConversationSummary {
  id: string;
  title: string;
  updatedAt: Date;
}
export interface ConversationDetail {
  id: string;
  title: string;
  messages: { role: 'user' | 'assistant'; content: string }[];
}
export interface SendResult {
  conversationId: string;
  title: string;
  reply: string;
}

// 할 일 추천 — 카테고리별 항목
export interface ChecklistSuggestionGroup {
  category: string;
  items: string[];
}

const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
// AI 요청 타임아웃 기본값(ms) — env AI_TIMEOUT_MS 로 재정의. 프론트와 동일 값을 사용한다.
const DEFAULT_AI_TIMEOUT_MS = 60_000;
const MAX_TOKENS = 1024;
const TITLE_MAX = 60;
// 컨텍스트로 주입할 담당 업무 최대 건수(토큰 절약)
const CONTEXT_TASK_LIMIT = 30;
// 컨텍스트로 주입할 프로젝트 최대 개수
const CONTEXT_PROJECT_LIMIT = 30;

// 주제 한정(가드레일) + 데이터 근거 지시
const SYSTEM_PROMPT = [
  '당신은 업무 협업 도구 "Relay" 의 AI 어시스턴트입니다.',
  '',
  '[역할]',
  "- 업무 관리, 일정·마감, 협업, Relay 사용법, 그리고 아래 '사용자 데이터' 에 관한 질문에 답합니다.",
  '- 업무와 무관한 주제(일반 상식, 시사, 번역, 코딩 과제, 잡담 등)는 정중히 거절하고 업무 관련 질문을 하도록 안내하세요.',
  '',
  '[규칙]',
  '- 한국어로 간결하고 명확하게 답하세요. 불필요한 인사말·사과·이모지 남발을 피하고, 과도한 존칭("회원님" 등) 대신 자연스러운 존댓말을 쓰세요.',
  '- 목록이나 강조가 필요하면 마크다운(굵게 **텍스트**, 목록 - 또는 1.)을 사용하세요. 표·복잡한 서식은 피하세요.',
  "- 아래 '사용자 데이터' 에 근거해 답하고, 데이터에 없는 내용은 추측하지 말고 모른다고 하세요.",
  "- 마감/일정은 제공된 '오늘 날짜' 를 기준으로 계산하세요.",
  '- 프로젝트는 한글 표시 제목으로 설명하세요.',
].join('\n');

// 업무 상태 → 한국어 라벨(컨텍스트 표기용)
const STATUS_LABEL: Record<TaskStatus, string> = {
  todo: '할 일',
  prog: '진행 중',
  review: '검토 대기',
  changes: '수정 요청',
  done: '완료',
};

// 할 일 추천용 시스템 프롬프트 — JSON 으로만 응답하도록 강제
const SUGGEST_SYSTEM_PROMPT = [
  '당신은 소프트웨어/업무 기획 보조자입니다.',
  "주어진 프로젝트와 업무 정보를 바탕으로, 이 업무(기능)를 완성하기 위해 필요한 '할 일(체크리스트)' 항목을 제안합니다.",
  '',
  '[규칙]',
  '- 반드시 아래 JSON 형식으로만 응답하세요. 설명·인사·마크다운 코드펜스 없이 순수 JSON 만 출력합니다.',
  '- 카테고리는 정확히 "필수 기능", "관리 기능", "보안 기능", "부가 기능" 4개를 이 순서대로 사용하세요. (다른 이름·추가/누락 카테고리 금지)',
  '- 각 카테고리에 필요한 항목을 충분히 제안하세요. 항목 수에 제한은 없습니다. 적합한 항목이 없으면 빈 배열([])로 두세요.',
  '- 각 항목은 한국어로 간결한 한 문장(체크리스트 항목)으로 작성합니다.',
  '- 업무·프로젝트 맥락에 구체적으로 맞추고 일반론은 피하세요.',
  '',
  '[형식]',
  '{"groups":[{"category":"필수 기능","items":["..."]},{"category":"관리 기능","items":["..."]},{"category":"보안 기능","items":["..."]},{"category":"부가 기능","items":["..."]}]}',
].join('\n');

// AI(Claude) 채팅 서비스 — 대화/메시지를 DB 에 보관하고 Anthropic Messages API 를 호출.
// 키(CLAUDE_API_KEY)는 서버 env 에만 두고 클라이언트에 노출하지 않는다.
@Injectable()
export class AiService {
  private readonly logger = new Logger(AiService.name);
  private readonly apiKey: string;
  private readonly model: string;
  private readonly timeoutMs: number;

  constructor(
    config: ConfigService,
    private readonly taskService: TaskService,
    private readonly projectService: ProjectService,
    @InjectRepository(AiConversation)
    private readonly convRepo: Repository<AiConversation>,
    @InjectRepository(AiMessage)
    private readonly msgRepo: Repository<AiMessage>,
  ) {
    this.apiKey = (config.get<string>('CLAUDE_API_KEY') ?? '').trim();
    this.model = (
      config.get<string>('CLAUDE_MODEL') ?? 'claude-sonnet-4-6'
    ).trim();
    // AI 타임아웃 — env AI_TIMEOUT_MS(ms), 미설정/비정상이면 기본값
    const t = Number(config.get<string>('AI_TIMEOUT_MS'));
    this.timeoutMs = Number.isFinite(t) && t > 0 ? t : DEFAULT_AI_TIMEOUT_MS;
    if (!this.apiKey) {
      this.logger.warn('CLAUDE_API_KEY 미설정 — AI 채팅 비활성화');
    }
  }

  get enabled(): boolean {
    return this.apiKey.length > 0;
  }

  // 내 대화 목록(최근순, 페이지네이션)
  async listConversations(
    userId: string,
    page?: number,
    size?: number,
  ): Promise<PaginatedResult<ConversationSummary>> {
    const params = resolvePage(page, size);
    const [rows, total] = await this.convRepo.findAndCount({
      where: { user: { id: userId } },
      order: { updatedAt: 'DESC' },
      skip: params.skip,
      take: params.take,
    });
    return paginated(
      rows.map((c) => ({ id: c.id, title: c.title, updatedAt: c.updatedAt })),
      total,
      params,
    );
  }

  // 대화 상세(본인 소유만) — 메시지 시간순
  async getConversation(
    userId: string,
    conversationId: string,
  ): Promise<ConversationDetail> {
    const conv = await this.getOwnedConversation(userId, conversationId);
    const messages = await this.msgRepo.find({
      where: { conversation: { id: conv.id } },
      order: { createdAt: 'ASC' },
    });
    return {
      id: conv.id,
      title: conv.title,
      messages: messages.map((m) => ({ role: m.role, content: m.content })),
    };
  }

  // 새 대화 시작 — 첫 메시지로 대화 생성 후 응답
  async startConversation(
    userId: string,
    content: string,
  ): Promise<SendResult> {
    this.ensureEnabled();
    const conv = await this.convRepo.save(
      this.convRepo.create({
        user: { id: userId },
        title: this.makeTitle(content),
      }),
    );
    return this.appendAndReply(conv, content, userId);
  }

  // 기존 대화에 메시지 추가 후 응답(본인 소유만)
  async addMessage(
    userId: string,
    conversationId: string,
    content: string,
  ): Promise<SendResult> {
    this.ensureEnabled();
    const conv = await this.getOwnedConversation(userId, conversationId);
    return this.appendAndReply(conv, content, userId);
  }

  // 할 일(체크리스트) 추천 — 프로젝트 설명 + 업무 제목/내용을 바탕으로 카테고리별 항목 제안
  async suggestChecklist(
    userId: string,
    input: { projectId: string; title: string; content?: string[] },
  ): Promise<{ groups: ChecklistSuggestionGroup[] }> {
    this.ensureEnabled();
    // 프로젝트 정보(이름/설명) — 인증 사용자면 열람 가능. 없으면 404.
    const project = await this.projectService.findOne(input.projectId, userId);
    const content = (input.content ?? []).join('\n').trim() || '(없음)';
    const userMsg = [
      `[프로젝트] 표시 제목: ${project.name}`,
      `설명: ${project.description ?? '(없음)'}`,
      '',
      `[업무] 제목: ${input.title}`,
      '내용:',
      content,
    ].join('\n');

    // 4개 카테고리 × 항목 수 무제한의 구조화 JSON 이라 토큰 여유를 넉넉히 준다
    // (부족하면 응답이 중간에 잘려 JSON 파싱이 실패할 수 있음)
    const raw = await this.callClaude(
      [{ role: 'user', content: userMsg }],
      SUGGEST_SYSTEM_PROMPT,
      4096,
    );
    return { groups: this.parseSuggestions(raw) };
  }

  // Claude 의 JSON 응답을 파싱·정규화(코드펜스/잡텍스트 방어)
  private parseSuggestions(raw: string): ChecklistSuggestionGroup[] {
    let text = raw
      .trim()
      .replace(/^```(?:json)?\s*/i, '')
      .replace(/```$/, '')
      .trim();
    const start = text.indexOf('{');
    const end = text.lastIndexOf('}');
    if (start >= 0 && end > start) text = text.slice(start, end + 1);

    let parsed: unknown;
    try {
      parsed = JSON.parse(text);
    } catch {
      this.logger.warn(
        `추천 JSON 파싱 실패(응답 잘림 가능) — 원본 ${raw.length}자: ${raw.slice(0, 300)}`,
      );
      throw this.suggestFailed();
    }

    const rawGroups = (parsed as { groups?: unknown }).groups;
    const groups: unknown[] = Array.isArray(rawGroups) ? rawGroups : [];
    const result: ChecklistSuggestionGroup[] = [];
    for (const g of groups.slice(0, 8)) {
      const obj = g as { category?: unknown; items?: unknown };
      const category =
        typeof obj.category === 'string' ? obj.category.trim() : '';
      if (!category || !Array.isArray(obj.items)) continue;
      // 카테고리당 항목 수 제한 없음 — 응답 길이는 max_tokens 가 자연 제한
      const items = obj.items
        .filter((x): x is string => typeof x === 'string')
        .map((s) => s.trim())
        .filter((s) => s.length > 0);
      if (items.length > 0) result.push({ category, items });
    }
    if (result.length === 0) {
      this.logger.warn(
        `추천 결과가 비어 있음 — 원본 ${raw.length}자: ${raw.slice(0, 300)}`,
      );
      throw this.suggestFailed();
    }
    return result;
  }

  private suggestFailed(): BusinessException {
    return new BusinessException(
      'BIZ_001',
      '추천을 생성하지 못했습니다. 잠시 후 다시 시도해 주세요.',
      HttpStatus.BAD_GATEWAY,
    );
  }

  // 대화 삭제(본인 소유만) — 메시지 CASCADE
  async deleteConversation(
    userId: string,
    conversationId: string,
  ): Promise<void> {
    const conv = await this.getOwnedConversation(userId, conversationId);
    await this.convRepo.remove(conv);
  }

  // 사용자 메시지 저장 → 전체 이력으로 Claude 호출 → 응답 저장 → 대화 updatedAt 갱신.
  // Claude 실패 시 방금 저장한 사용자 메시지를 롤백(빈 대화면 대화도 삭제)해 고아 데이터 방지.
  private async appendAndReply(
    conv: AiConversation,
    content: string,
    userId: string,
  ): Promise<SendResult> {
    const userMsg = await this.msgRepo.save(
      this.msgRepo.create({
        conversation: { id: conv.id },
        role: 'user',
        content,
      }),
    );
    const history = await this.msgRepo.find({
      where: { conversation: { id: conv.id } },
      order: { createdAt: 'ASC' },
    });

    let reply: string;
    try {
      const system = await this.buildSystemPrompt(userId);
      reply = await this.callClaude(
        history.map((m) => ({ role: m.role, content: m.content })),
        system,
      );
    } catch (e) {
      // 롤백 — 방금 사용자 메시지 제거, 남은 메시지가 없으면(첫 메시지였다면) 대화도 삭제
      await this.msgRepo.delete(userMsg.id);
      const remaining = await this.msgRepo.count({
        where: { conversation: { id: conv.id } },
      });
      if (remaining === 0) {
        await this.convRepo.delete(conv.id);
      }
      throw e;
    }

    await this.msgRepo.save(
      this.msgRepo.create({
        conversation: { id: conv.id },
        role: 'assistant',
        content: reply,
      }),
    );
    // 최근 대화로 정렬되도록 updatedAt 갱신(@UpdateDateColumn)
    await this.convRepo.save(conv);
    return { conversationId: conv.id, title: conv.title, reply };
  }

  // 소유권 확인 — 본인 대화가 아니면 404(존재 비노출)
  private async getOwnedConversation(
    userId: string,
    conversationId: string,
  ): Promise<AiConversation> {
    const conv = await this.convRepo.findOne({
      where: { id: conversationId, user: { id: userId } },
    });
    if (!conv) {
      throw new BusinessException(
        'RES_001',
        '대화를 찾을 수 없습니다.',
        HttpStatus.NOT_FOUND,
      );
    }
    return conv;
  }

  private makeTitle(content: string): string {
    const firstLine = content.trim().split('\n')[0].trim();
    if (!firstLine) return '새 대화';
    return firstLine.length > TITLE_MAX
      ? `${firstLine.slice(0, TITLE_MAX)}…`
      : firstLine;
  }

  private ensureEnabled(): void {
    if (!this.enabled) {
      throw new BusinessException(
        'BIZ_001',
        'AI 기능이 설정되지 않았습니다. 관리자에게 문의해 주세요.',
        HttpStatus.SERVICE_UNAVAILABLE,
      );
    }
  }

  // 시스템 프롬프트 = 가드레일 + 현재 사용자 데이터 컨텍스트
  private async buildSystemPrompt(userId: string): Promise<string> {
    const context = await this.buildUserContext(userId);
    return `${SYSTEM_PROMPT}\n\n--- 사용자 데이터 ---\n${context}`;
  }

  // 현재 사용자의 담당/지시 업무를 요약해 컨텍스트 문자열로 — 실패해도 채팅은 진행
  private async buildUserContext(userId: string): Promise<string> {
    const today = new Date(Date.now() + 9 * 60 * 60 * 1000)
      .toISOString()
      .slice(0, 10); // KST 기준 오늘
    try {
      const assigned = await this.taskService.listAssigned(
        userId,
        1,
        CONTEXT_TASK_LIMIT,
      );
      const issued = await this.taskService.listIssued(userId, 1, 1);
      const projects = await this.projectService.findAll(
        1,
        CONTEXT_PROJECT_LIMIT,
      );

      const lines = [`오늘 날짜: ${today} (KST)`, ''];

      // 담당 업무
      lines.push(`내가 담당한 업무 (${assigned.total}건):`);
      if (assigned.items.length === 0) {
        lines.push('- (없음)');
      } else {
        for (const t of assigned.items) {
          const due = t.dueDate ? t.dueDate.slice(0, 10) : '없음';
          const [done, totalC] = t.checklist;
          lines.push(
            `- [${t.projectName}] #${t.id} ${t.title} — 상태: ${STATUS_LABEL[t.status]}, 마감: ${due}, 체크리스트: ${done}/${totalC}`,
          );
        }
        if (assigned.total > assigned.items.length) {
          lines.push(`- … 외 ${assigned.total - assigned.items.length}건`);
        }
      }
      lines.push('', `내가 지시한 업무: ${issued.total}건`);

      // 프로젝트 목록(인증 사용자는 전체 열람 가능)
      lines.push('', `프로젝트 목록 (${projects.total}개):`);
      if (projects.items.length === 0) {
        lines.push('- (없음)');
      } else {
        for (const p of projects.items) {
          lines.push(`- ${p.name} — 업무 ${p.doneCount}/${p.totalCount}건`);
        }
        if (projects.total > projects.items.length) {
          lines.push(`- … 외 ${projects.total - projects.items.length}개`);
        }
      }
      return lines.join('\n');
    } catch (e) {
      this.logger.warn(
        `사용자 컨텍스트 조회 실패: ${e instanceof Error ? e.message : String(e)}`,
      );
      return `오늘 날짜: ${today} (KST)\n(사용자 업무 데이터를 불러오지 못했습니다.)`;
    }
  }

  // Anthropic Messages API 호출 — 실패 시 내부/키 노출 없이 일반 문구
  private async callClaude(
    messages: { role: 'user' | 'assistant'; content: string }[],
    system: string,
    maxTokens: number = MAX_TOKENS,
  ): Promise<string> {
    let response: Response;
    try {
      response = await fetch(ANTHROPIC_URL, {
        method: 'POST',
        headers: {
          'content-type': 'application/json',
          'x-api-key': this.apiKey,
          'anthropic-version': '2023-06-01',
        },
        body: JSON.stringify({
          model: this.model,
          max_tokens: maxTokens,
          system,
          messages,
        }),
        signal: AbortSignal.timeout(this.timeoutMs),
      });
    } catch (e) {
      this.logger.error(
        `Anthropic 요청 실패: ${e instanceof Error ? e.message : String(e)}`,
      );
      throw new BusinessException(
        'BIZ_001',
        'AI 응답을 가져오지 못했습니다. 잠시 후 다시 시도해 주세요.',
        HttpStatus.BAD_GATEWAY,
      );
    }

    if (!response.ok) {
      const detail = await response.text().catch(() => '');
      this.logger.error(
        `Anthropic API 오류 ${response.status}: ${detail.slice(0, 500)}`,
      );
      throw new BusinessException(
        'BIZ_001',
        'AI 응답을 가져오지 못했습니다. 잠시 후 다시 시도해 주세요.',
        HttpStatus.BAD_GATEWAY,
      );
    }

    const data = (await response.json()) as AnthropicResponse;
    const text = (data.content ?? [])
      .filter((b) => b.type === 'text' && typeof b.text === 'string')
      .map((b) => b.text)
      .join('')
      .trim();
    return text || '(빈 응답을 받았습니다.)';
  }
}
