/**
 * ============================================================
 * Outlook (클래식 데스크톱) — COM 자동화 via PowerShell (v2.2.269)
 *
 * 사용자 PC에 설치된 클래식 Outlook 을 COM 으로 직접 읽는다 — 별도 인증 없음
 * (Outlook 이 이미 로그인돼 있으면 그대로 동작). Graph API 미사용 (Azure 앱
 * 등록·관리자 동의가 필요해 개인/사내 환경에서 부담).
 *
 * 안전 원칙:
 *  - 읽기 + '초안 저장(Drafts)' 만. **발송(Send) 코드는 이 파일에 존재하지 않는다.**
 *  - EntryID 는 우리가 목록에서 받은 hex 문자열만 인라인 (임의 입력 스크립트 주입 차단).
 *  - 자유 텍스트(초안 본문)는 Base64 로 넘겨 PowerShell 인용 문제 원천 차단.
 *
 * 제약: "새 Outlook" (Monarch) 은 COM 미지원 — 클래식 Outlook 전용. 미설치/미실행
 * 환경이면 COM 생성 단계에서 에러 → 호출자가 안내 메시지로 변환.
 * ============================================================
 */

import { execFile } from 'child_process';

export interface OutlookMail {
    entryId: string;
    subject: string;
    from: string;
    to?: string;
    /** 'YYYY-MM-DD HH:mm' (로컬). */
    received: string;
    unread: boolean;
    snippet: string;
    body?: string;
    /** 받은편지함 하위 폴더에서 수집된 경우 폴더명 (루트는 빈 값). v2.2.281 */
    folder?: string;
    /** 내가 To 가 아니라 CC/참조로만 수신 (v2.2.290) — 회신 대상 아님 → 확인만 분류. */
    ccOnly?: boolean;
}

type OlResult<T> = { ok: true; data: T } | { ok: false; error: string };

const OUTLOOK_HINT =
    '클래식 Outlook COM 연결 실패 — 데스크톱 Outlook(클래식)이 설치·로그인돼 있어야 합니다. ' +
    '"새 Outlook"(New Outlook 토글 ON)은 COM 을 지원하지 않으니 클래식 모드로 전환하세요.';

/** PowerShell 스크립트 실행 → stdout(UTF-8). COM 초기화 포함 공통 프리앰블. */
function runPs(script: string, timeoutMs = 30_000): Promise<OlResult<string>> {
    const full = [
        '$ErrorActionPreference = "Stop"',
        '[Console]::OutputEncoding = [System.Text.Encoding]::UTF8',
        'try {',
        '  $ol = New-Object -ComObject Outlook.Application',
        '  $ns = $ol.GetNamespace("MAPI")',
        script,
        '} catch { Write-Error $_.Exception.Message; exit 1 }',
    ].join('\n');
    return new Promise((resolve) => {
        execFile(
            'powershell.exe',
            ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', full],
            { timeout: timeoutMs, maxBuffer: 20 * 1024 * 1024, windowsHide: true },
            (err, stdout, stderr) => {
                if (err) {
                    const msg = (stderr || err.message || '').trim();
                    const comFail = /80080005|REGDB_E_CLASSNOTREG|Outlook\.Application|CO_E_SERVER_EXEC_FAILURE/i.test(msg);
                    resolve({ ok: false, error: comFail ? OUTLOOK_HINT : (msg || 'PowerShell 실행 실패') });
                    return;
                }
                resolve({ ok: true, data: (stdout || '').trim() });
            },
        );
    });
}

/** ConvertTo-Json 이 단일 항목일 때 객체를 주므로 배열로 정규화. */
function asArray(parsed: unknown): any[] {
    if (Array.isArray(parsed)) return parsed;
    if (parsed && typeof parsed === 'object') return [parsed];
    return [];
}

/** EntryID 검증 — Outlook EntryID 는 hex 문자열. 스크립트 인라인 전 필수. */
function safeEntryId(id: string): string | null {
    return /^[0-9A-Fa-f]{8,}$/.test(id) ? id : null;
}

/**
 * 받은편지함 최근 목록 (기본 7일, 최대 max건). 전체 스캔은 days≤3650·max≤300.
 * v2.2.281: includeSubfolders(기본 true)면 받은편지함의 **모든 하위 폴더(중첩 포함)**를
 * BFS 로 순회 — 규칙(필터)으로 분류된 메일도 수집된다. 폴더별로 최신순 정렬 후
 * 기간 컷, 전체 병합 → 최신순 → max 컷. 폴더 수는 120개 가드.
 */
export async function listInbox(days = 7, max = 25, includeSubfolders = true): Promise<OlResult<OutlookMail[]>> {
    const d = Math.max(1, Math.min(3650, Math.floor(days)));
    const m = Math.max(1, Math.min(300, Math.floor(max)));
    const script = `
  # v2.2.290: 모든 계정(Store)의 받은편지함 수집 — 다중 계정에서 메일 누락 방지.
  $myName = ""
  try { $myName = "" + $ns.CurrentUser.Name } catch {}
  $inboxes = New-Object System.Collections.ArrayList
  foreach ($store in $ns.Stores) {
    try { [void]$inboxes.Add($store.GetDefaultFolder(6)) } catch {}
  }
  if ($inboxes.Count -eq 0) { [void]$inboxes.Add($ns.GetDefaultFolder(6)) }
  $rootIds = New-Object System.Collections.ArrayList
  $targets = New-Object System.Collections.ArrayList
  foreach ($ib in $inboxes) { [void]$targets.Add($ib); [void]$rootIds.Add($ib.EntryID) }
  ${includeSubfolders ? `
  $queue = New-Object System.Collections.Queue
  foreach ($ib in $inboxes) { $queue.Enqueue($ib) }
  while ($queue.Count -gt 0 -and $targets.Count -lt 120) {
    $f = $queue.Dequeue()
    foreach ($sub in $f.Folders) {
      if ($targets.Count -ge 120) { break }
      [void]$targets.Add($sub)
      $queue.Enqueue($sub)
    }
  }` : ''}
  $cut = (Get-Date).AddDays(-${d})
  $out = @()
  foreach ($folder in $targets) {
    $isRoot = $rootIds.Contains($folder.EntryID)
    $items = $folder.Items
    try { $items.Sort("[ReceivedTime]", $true) } catch { continue }
    $count = 0
    foreach ($it in $items) {
      if ($count -ge ${m}) { break }
      if ($it.MessageClass -notlike "IPM.Note*") { continue }
      $rt = $null
      try { $rt = $it.ReceivedTime } catch {}
      if (-not $rt) { continue }
      if ($rt -lt $cut) { break }
      $b = ""
      try { $b = ($it.Body -replace "\\s+", " ").Trim() } catch {}
      $sn = $b.Substring(0, [Math]::Min(120, $b.Length))
      # To/CC 판별 — To 문자열에 내 이름이 없으면 CC/참조 수신 (내 이름 미확인 시 To 로 간주)
      $toStr = ""
      try { $toStr = "" + $it.To } catch {}
      $ccOnly = $false
      if ($myName -ne "" -and $toStr -ne "") { $ccOnly = -not ($toStr -like ("*" + $myName + "*")) }
      $out += [pscustomobject]@{
        entryId = $it.EntryID
        subject = "" + $it.Subject
        from = "" + $it.SenderName
        received = $rt.ToString("yyyy-MM-dd HH:mm")
        unread = [bool]$it.UnRead
        snippet = $sn
        folder = if ($isRoot) { "" } else { "" + $folder.Name }
        ccOnly = $ccOnly
      }
      $count++
    }
  }
  $out | Sort-Object -Property received -Descending | Select-Object -First ${m} | ConvertTo-Json -Depth 3 -Compress`;
    const r = await runPs(script, 90_000);
    if (!r.ok) return r;
    if (!r.data) return { ok: true, data: [] };
    try {
        return { ok: true, data: asArray(JSON.parse(r.data)) as OutlookMail[] };
    } catch {
        return { ok: false, error: 'Outlook 응답 JSON 파싱 실패' };
    }
}

/**
 * 보낸편지함(Sent Items, folder 5) 최근 목록 — 브리핑의 '답변 여부' 판정용.
 * 같은 주제의 마지막 발신 시각을 알아야 "내가 회신했는지"를 판단할 수 있다.
 */
export async function listSent(days = 7, max = 40): Promise<OlResult<OutlookMail[]>> {
    const d = Math.max(1, Math.min(3650, Math.floor(days)));
    const m = Math.max(1, Math.min(200, Math.floor(max)));
    const script = `
  # v2.2.290: 모든 계정(Store)의 보낸편지함 — 다른 계정에서 보낸 회신도 답변으로 인식.
  $sents = New-Object System.Collections.ArrayList
  foreach ($store in $ns.Stores) {
    try { [void]$sents.Add($store.GetDefaultFolder(5)) } catch {}
  }
  if ($sents.Count -eq 0) { [void]$sents.Add($ns.GetDefaultFolder(5)) }
  $cut = (Get-Date).AddDays(-${d})
  $out = @()
  foreach ($sent in $sents) {
    $items = $sent.Items
    try { $items.Sort("[SentOn]", $true) } catch { continue }
    $count = 0
    foreach ($it in $items) {
      if ($count -ge ${m}) { break }
      if ($it.MessageClass -notlike "IPM.Note*") { continue }
      $when = $null
      try { $when = $it.SentOn } catch {}
      if (-not $when) { continue }
      if ($when -lt $cut) { break }
      $out += [pscustomobject]@{
        entryId = $it.EntryID
        subject = "" + $it.Subject
        from = "나"
        to = "" + $it.To
        received = $when.ToString("yyyy-MM-dd HH:mm")
        unread = $false
        snippet = ""
      }
      $count++
    }
  }
  $out | Sort-Object -Property received -Descending | Select-Object -First ${m} | ConvertTo-Json -Depth 3 -Compress`;
    const r = await runPs(script);
    if (!r.ok) return r;
    if (!r.data) return { ok: true, data: [] };
    try {
        return { ok: true, data: asArray(JSON.parse(r.data)) as OutlookMail[] };
    } catch {
        return { ok: false, error: 'Outlook 응답 JSON 파싱 실패' };
    }
}

/**
 * 메일을 Outlook 창으로 직접 연다 (v2.2.289) — 브리핑의 [O번호]에서 실제 메일로
 * 점프하는 다리. Display() 는 읽기용 인스펙터 창만 띄운다 (발송·수정 없음).
 */
export async function openMailInOutlook(entryId: string): Promise<OlResult<true>> {
    const id = safeEntryId(entryId);
    if (!id) return { ok: false, error: '잘못된 EntryID' };
    const script = `
  $it = $ns.GetItemFromID("${id}")
  $it.Display()
  "OK"`;
    const r = await runPs(script);
    if (!r.ok) return r;
    return r.data.includes('OK') ? { ok: true, data: true } : { ok: false, error: '열기 확인 실패' };
}

/** 메일 1건 전체 읽기 (본문 12,000자 컷). */
export async function readMail(entryId: string): Promise<OlResult<OutlookMail>> {
    const id = safeEntryId(entryId);
    if (!id) return { ok: false, error: '잘못된 EntryID' };
    const script = `
  $it = $ns.GetItemFromID("${id}")
  $b = "" + $it.Body
  [pscustomobject]@{
    entryId = $it.EntryID
    subject = "" + $it.Subject
    from = "" + $it.SenderName
    to = "" + $it.To
    received = $it.ReceivedTime.ToString("yyyy-MM-dd HH:mm")
    unread = [bool]$it.UnRead
    snippet = ""
    body = $b.Substring(0, [Math]::Min(12000, $b.Length))
  } | ConvertTo-Json -Depth 3 -Compress`;
    const r = await runPs(script);
    if (!r.ok) return r;
    try {
        const arr = asArray(JSON.parse(r.data));
        if (!arr.length) return { ok: false, error: '메일을 찾지 못했습니다' };
        return { ok: true, data: arr[0] as OutlookMail };
    } catch {
        return { ok: false, error: 'Outlook 응답 JSON 파싱 실패' };
    }
}

/**
 * 회신 초안을 **Drafts(임시 보관함)에 저장만** 한다 — 발송하지 않음.
 * 본문은 Base64 로 전달 (한글·따옴표·개행 안전).
 */
export async function saveReplyDraft(entryId: string, bodyText: string): Promise<OlResult<{ savedTo: string }>> {
    const id = safeEntryId(entryId);
    if (!id) return { ok: false, error: '잘못된 EntryID' };
    const b64 = Buffer.from(bodyText, 'utf-8').toString('base64');
    const script = `
  $it = $ns.GetItemFromID("${id}")
  $reply = $it.Reply()
  $draft = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String("${b64}"))
  $reply.Body = $draft + "\`r\`n\`r\`n" + $reply.Body
  $reply.Save()
  "OK"`;
    const r = await runPs(script);
    if (!r.ok) return r;
    return r.data.includes('OK')
        ? { ok: true, data: { savedTo: '임시 보관함(Drafts)' } }
        : { ok: false, error: '초안 저장 확인 실패' };
}
