/**
 * OfficeSnapshot — Astra Office 의 도메인 타입.
 *
 * 동시성 진실 (docs/ASTRA_OFFICE_REFACTOR.md §1): dispatcher 는 직렬이라 한 시점에
 * active agent 는 0 또는 1명. 이걸 데이터로 강제하는 게 이 타입의 핵심 역할.
 *
 * 이 세션에서는 *타입 + validator + empty factory* 만. 백엔드 emit 측 wiring 은
 * 다음 세션에서 단계적으로 옮긴다. 현재는 옛 AgentWorkState/CompanyTurnEvent 가
 * presenter 입력으로 살아있고, 출력만 OfficeSnapshot.
 */

import type { AgentStatus } from '../company/pixelOfficeState';

export type OfficePhase =
    | 'idle'
    | 'intake'
    | 'planning'
    | 'executing'
    | 'reviewing'
    | 'awaiting-approval'
    | 'reporting'
    | 'done'
    | 'error';

/** 한 명의 agent 가 한 시점에 가진 상태. */
export interface OfficeAgentSnapshot {
    /** company roster 의 정식 id (built-in 또는 custom). */
    agentId: string;
    /** 표시 이름. */
    agentName: string;
    /** 책상 색깔 / 매핑에 쓰이는 role category. */
    roleCategory:
        | 'ceo'
        | 'planner'
        | 'researcher'
        | 'designer'
        | 'developer'
        | 'qa'
        | 'inspector'
        | 'support'
        | 'writer';
    status: AgentStatus;
    /** "검수 라운드 2/3", "타입 누락" 같은 짧은 부가 정보. */
    currentStep?: string;
    /** 머리 위 말풍선 source. presenter 가 비워서 보내면 webview 는 풍선 안 띄움. */
    lastLog?: string;
    /** 정렬 / 신선도 판정. */
    lastActivityAt: number;
}

export interface OfficeActivityItem {
    /** epoch ms. */
    ts: number;
    agentId: string;
    text: string;
    kind?: 'ok' | 'warn' | 'err' | 'info';
}

/** webview 가 풍선을 띄울 때 참고할 hint. presenter 가 매 update 마다 새 풍선 후보를 만들어 보냄. */
export interface OfficeBubbleSeed {
    agentId: string;
    text: string;
    /** 색깔/스타일. presenter 가 status/event/warning/error/success 중 분류. */
    type: 'status' | 'event' | 'warning' | 'error' | 'success';
}

export interface OfficeSnapshot {
    /** 회사 전체 phase. activeAgent 가 어떤 단계에 있는지와 분리해서 추적. */
    phase: OfficePhase;
    /** 활성 agent id — 직렬 dispatch 가정. null 이면 idle. */
    activeAgentId: string | null;
    /** 이 turn 에 참여 가능한 모든 agent. webview 의 책상 그리드 source. */
    roster: OfficeAgentSnapshot[];
    /** 현재 요구사항 / 계약 요약. */
    task?: {
        goal: string;
        context?: string;
        format?: string;
        criteria?: string[];
        openQuestions?: string[];
    };
    /** 파이프라인 모드의 stage 진행도. */
    pipeline?: {
        stages: Array<{
            label: string;
            agentId?: string;
            status: 'done' | 'active' | 'pending';
        }>;
        index: number;
    };
    /** 승인 / 추가 정보 대기. */
    awaiting?: {
        kind: 'approval' | 'clarification';
        questions: string[];
    };
    /** 누적 활동 ring buffer — webview ticker 의 source. */
    activity: OfficeActivityItem[];
    /** 이 update 사이클에서 새로 생긴 풍선 후보. presenter 가 매 emit 마다 갱신. */
    newBubbles: OfficeBubbleSeed[];
    /** 디버깅/정렬용. */
    updatedAt: number;
}

const VALID_PHASES: ReadonlySet<OfficePhase> = new Set<OfficePhase>([
    'idle',
    'intake',
    'planning',
    'executing',
    'reviewing',
    'awaiting-approval',
    'reporting',
    'done',
    'error',
]);

const VALID_ROLES: ReadonlySet<OfficeAgentSnapshot['roleCategory']> = new Set([
    'ceo',
    'planner',
    'researcher',
    'designer',
    'developer',
    'qa',
    'inspector',
    'support',
    'writer',
]);

const VALID_STATUSES: ReadonlySet<AgentStatus> = new Set<AgentStatus>([
    'idle',
    'intake',
    'analyzing',
    'need_clarification',
    'contract_ready',
    'planning',
    'executing',
    'reviewing',
    'waiting_approval',
    'error',
    'done',
]);

const VALID_BUBBLE_TYPES = new Set<OfficeBubbleSeed['type']>([
    'status',
    'event',
    'warning',
    'error',
    'success',
]);

const VALID_KINDS = new Set<NonNullable<OfficeActivityItem['kind']>>([
    'ok',
    'warn',
    'err',
    'info',
]);

/**
 * 런타임 schema validation. 임의의 unknown 입력을 받아 *완전한 OfficeSnapshot* 또는
 * null 을 반환한다. 누락된 필드는 안전한 기본값으로 채운다. 잘못된 enum 값은 'idle'/
 * 기본 role 로 fall through. 호출자는 null 이면 default snapshot 으로 폴백 권장.
 */
export function validateOfficeSnapshot(raw: unknown): OfficeSnapshot | null {
    if (!raw || typeof raw !== 'object') return null;
    const r = raw as Record<string, unknown>;

    const phaseRaw = String(r.phase ?? 'idle');
    const phase: OfficePhase = (VALID_PHASES as ReadonlySet<string>).has(phaseRaw)
        ? (phaseRaw as OfficePhase)
        : 'idle';

    const activeAgentId =
        typeof r.activeAgentId === 'string' && r.activeAgentId.length > 0
            ? r.activeAgentId
            : null;

    const rosterRaw = Array.isArray(r.roster) ? r.roster : [];
    const roster: OfficeAgentSnapshot[] = rosterRaw
        .map((a) => _validateAgent(a))
        .filter((a): a is OfficeAgentSnapshot => a !== null);

    const taskRaw = r.task as Record<string, unknown> | undefined;
    const task = taskRaw && typeof taskRaw === 'object' && typeof taskRaw.goal === 'string'
        ? {
              goal: taskRaw.goal,
              context: _optStr(taskRaw.context),
              format: _optStr(taskRaw.format),
              criteria: Array.isArray(taskRaw.criteria)
                  ? taskRaw.criteria.filter((x): x is string => typeof x === 'string')
                  : undefined,
              openQuestions: Array.isArray(taskRaw.openQuestions)
                  ? taskRaw.openQuestions.filter((x): x is string => typeof x === 'string')
                  : undefined,
          }
        : undefined;

    const pipelineRaw = r.pipeline as Record<string, unknown> | undefined;
    const pipeline = pipelineRaw && Array.isArray(pipelineRaw.stages)
        ? {
              stages: pipelineRaw.stages
                  .map((s) => _validateStage(s))
                  .filter((s): s is { label: string; agentId?: string; status: 'done' | 'active' | 'pending' } => s !== null),
              index: typeof pipelineRaw.index === 'number' ? pipelineRaw.index : 0,
          }
        : undefined;

    const awaitingRaw = r.awaiting as Record<string, unknown> | undefined;
    const awaiting = awaitingRaw && (awaitingRaw.kind === 'approval' || awaitingRaw.kind === 'clarification')
        ? {
              kind: awaitingRaw.kind as 'approval' | 'clarification',
              questions: Array.isArray(awaitingRaw.questions)
                  ? awaitingRaw.questions.filter((x): x is string => typeof x === 'string')
                  : [],
          }
        : undefined;

    const activity: OfficeActivityItem[] = Array.isArray(r.activity)
        ? r.activity
              .map((it) => _validateActivity(it))
              .filter((it): it is OfficeActivityItem => it !== null)
        : [];

    const newBubbles: OfficeBubbleSeed[] = Array.isArray(r.newBubbles)
        ? r.newBubbles
              .map((b) => _validateBubble(b))
              .filter((b): b is OfficeBubbleSeed => b !== null)
        : [];

    const updatedAt = typeof r.updatedAt === 'number' ? r.updatedAt : Date.now();

    return { phase, activeAgentId, roster, task, pipeline, awaiting, activity, newBubbles, updatedAt };
}

export function makeEmptyOfficeSnapshot(): OfficeSnapshot {
    return {
        phase: 'idle',
        activeAgentId: null,
        roster: [],
        activity: [],
        newBubbles: [],
        updatedAt: Date.now(),
    };
}

// ─────────── helpers ───────────

function _optStr(v: unknown): string | undefined {
    return typeof v === 'string' ? v : undefined;
}

function _validateAgent(raw: unknown): OfficeAgentSnapshot | null {
    if (!raw || typeof raw !== 'object') return null;
    const a = raw as Record<string, unknown>;
    if (typeof a.agentId !== 'string' || !a.agentId) return null;

    const role = typeof a.roleCategory === 'string' && (VALID_ROLES as ReadonlySet<string>).has(a.roleCategory)
        ? (a.roleCategory as OfficeAgentSnapshot['roleCategory'])
        : 'support';
    const status: AgentStatus = typeof a.status === 'string' && (VALID_STATUSES as ReadonlySet<string>).has(a.status)
        ? (a.status as AgentStatus)
        : 'idle';

    return {
        agentId: a.agentId,
        agentName: typeof a.agentName === 'string' ? a.agentName : a.agentId,
        roleCategory: role,
        status,
        currentStep: _optStr(a.currentStep),
        lastLog: _optStr(a.lastLog),
        lastActivityAt: typeof a.lastActivityAt === 'number' ? a.lastActivityAt : 0,
    };
}

function _validateStage(raw: unknown): { label: string; agentId?: string; status: 'done' | 'active' | 'pending' } | null {
    if (!raw || typeof raw !== 'object') return null;
    const s = raw as Record<string, unknown>;
    if (typeof s.label !== 'string') return null;
    const statusRaw = String(s.status ?? 'pending');
    const status = (statusRaw === 'done' || statusRaw === 'active' || statusRaw === 'pending') ? statusRaw : 'pending';
    return { label: s.label, agentId: _optStr(s.agentId), status };
}

function _validateActivity(raw: unknown): OfficeActivityItem | null {
    if (!raw || typeof raw !== 'object') return null;
    const a = raw as Record<string, unknown>;
    if (typeof a.text !== 'string' || typeof a.agentId !== 'string') return null;
    const kind = typeof a.kind === 'string' && (VALID_KINDS as ReadonlySet<string>).has(a.kind)
        ? (a.kind as OfficeActivityItem['kind'])
        : undefined;
    return {
        ts: typeof a.ts === 'number' ? a.ts : Date.now(),
        agentId: a.agentId,
        text: a.text,
        kind,
    };
}

function _validateBubble(raw: unknown): OfficeBubbleSeed | null {
    if (!raw || typeof raw !== 'object') return null;
    const b = raw as Record<string, unknown>;
    if (typeof b.agentId !== 'string' || typeof b.text !== 'string') return null;
    const type = typeof b.type === 'string' && (VALID_BUBBLE_TYPES as ReadonlySet<string>).has(b.type)
        ? (b.type as OfficeBubbleSeed['type'])
        : 'status';
    return { agentId: b.agentId, text: b.text, type };
}
