/**
 * Google Calendar API v3 — event create/list 호출.
 *
 * access token 은 caller 가 직접 주입한다. 만료 처리는 `withFreshAccessToken`
 * 헬퍼가 refresh token 으로 갱신 → 호출 → 401 발생 시 한 번 더 갱신 + 재시도.
 *
 * 외부 라이브러리(googleapis) 안 씀 — Calendar API 는 REST 라 native fetch 면 충분.
 */

import * as vscode from 'vscode';
import { refreshAccessToken } from './oauth';
import { readCalendarConfig, writeCalendarConfig } from './calendarCache';

const API_BASE = 'https://www.googleapis.com/calendar/v3';

export interface CalendarEventInput {
    /** 일정 제목 (필수). */
    title: string;
    /** ISO 시작 시각 — 'YYYY-MM-DDTHH:MM' (로컬) 또는 'YYYY-MM-DDTHH:MM:SS±HH:MM' (timezone 포함). */
    start: string;
    /** ISO 종료 시각. 없으면 duration(분) 으로부터 계산. duration 도 없으면 60분. */
    end?: string;
    /** end 없을 때 시작부터 이만큼 (분 단위, default 60). */
    durationMinutes?: number;
    description?: string;
    location?: string;
    /** all-day 일정 여부 — true 면 start 는 'YYYY-MM-DD' 만 받음. */
    allDay?: boolean;
}

export interface CreatedEvent {
    /** Google 이 발급한 event id. */
    id: string;
    /** Google Calendar 웹에서 열 수 있는 URL. */
    htmlLink: string;
    /** API 가 echo 해준 시작 시각. */
    startIso: string;
    title: string;
}

/**
 * 일정 생성. config 에 refresh token 이 있어야 함. access token 자동 갱신.
 *
 * 반환값:
 *   ok: true → CreatedEvent
 *   ok: false → 에러 메시지 (UI 표시용)
 */
export async function createCalendarEvent(
    context: vscode.ExtensionContext,
    input: CalendarEventInput,
): Promise<{ ok: true; event: CreatedEvent } | { ok: false; error: string }> {
    const cfg = readCalendarConfig(context);
    const tokenResult = await _getFreshAccessToken(context);
    if (!tokenResult.ok) return { ok: false, error: tokenResult.error };

    const body = _buildEventBody(input, cfg.defaultDurationMinutes ?? 60);
    if (!body.ok) return { ok: false, error: body.error };

    const calId = (cfg.calendarId || 'primary').trim() || 'primary';
    const url = `${API_BASE}/calendars/${encodeURIComponent(calId)}/events`;
    try {
        const res = await fetch(url, {
            method: 'POST',
            headers: {
                Authorization: `Bearer ${tokenResult.accessToken}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(body.event),
            signal: AbortSignal.timeout(15000),
        });
        const json: any = await res.json().catch(() => ({}));
        if (!res.ok) {
            const msg = json?.error?.message || `HTTP ${res.status}`;
            return { ok: false, error: msg };
        }
        return {
            ok: true,
            event: {
                id: json.id,
                htmlLink: json.htmlLink,
                startIso: json.start?.dateTime ?? json.start?.date ?? input.start,
                title: input.title,
            },
        };
    } catch (e: any) {
        return { ok: false, error: e?.message ?? String(e) };
    }
}

/** Calendar API 요청 body 빌더 — 단위테스트 가능하도록 분리. */
export function _buildEventBody(
    input: CalendarEventInput,
    fallbackDurationMin: number,
): { ok: true; event: any } | { ok: false; error: string } {
    if (!input.title || !input.title.trim()) return { ok: false, error: 'title 비어있음' };
    if (!input.start || !input.start.trim()) return { ok: false, error: 'start 비어있음' };

    if (input.allDay) {
        // all-day: date 형식만 (YYYY-MM-DD). end 는 exclusive 라 다음 날.
        const startDate = input.start.slice(0, 10);
        const endDate = input.end ? input.end.slice(0, 10) : _addDaysDate(startDate, 1);
        return {
            ok: true,
            event: {
                summary: input.title.trim(),
                description: input.description || undefined,
                location: input.location || undefined,
                start: { date: startDate },
                end: { date: endDate },
                reminders: { useDefault: true },
            },
        };
    }

    let endIso: string | undefined = input.end;
    if (!endIso) {
        const dur = (input.durationMinutes && input.durationMinutes > 0) ? input.durationMinutes : fallbackDurationMin;
        const computed = _addMinutesIso(input.start, dur);
        if (!computed) return { ok: false, error: `start 시각 형식 오류: ${input.start}` };
        endIso = computed;
    }

    // Google Calendar 는 timezone 정보가 없으면 timeZone 필드 별도 필요.
    // 'YYYY-MM-DDTHH:MM' 처럼 timezone 빠진 입력은 OS 로컬 timezone 으로 가정.
    const hasOffset = /([+-]\d{2}:\d{2}|Z)$/.test(input.start);
    const timeZone = hasOffset ? undefined : Intl.DateTimeFormat().resolvedOptions().timeZone;

    return {
        ok: true,
        event: {
            summary: input.title.trim(),
            description: input.description || undefined,
            location: input.location || undefined,
            start: { dateTime: input.start, ...(timeZone ? { timeZone } : {}) },
            end: { dateTime: endIso, ...(timeZone ? { timeZone } : {}) },
            reminders: {
                useDefault: false,
                overrides: [
                    { method: 'popup', minutes: 5 },
                    { method: 'popup', minutes: 60 },
                ],
            },
        },
    };
}

/** 'YYYY-MM-DDTHH:MM[:SS][±HH:MM|Z]' 에 분 더해 ISO 반환. 잘못된 형식이면 null. */
export function _addMinutesIso(startIso: string, minutes: number): string | null {
    // 안전한 파싱: 명시적 정규식 → Date → ISO 재조립. timezone 정보 보존.
    const m = startIso.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})(:\d{2})?([+-]\d{2}:\d{2}|Z)?$/);
    if (!m) return null;
    const [, base, sec, tz] = m;
    const full = `${base}${sec ?? ':00'}${tz ?? ''}`;
    const t = new Date(full);
    if (Number.isNaN(t.getTime())) return null;
    const out = new Date(t.getTime() + minutes * 60 * 1000);
    // 원본이 timezone 정보 없는 로컬 시각이면 같은 포맷으로 돌려준다.
    if (!tz) {
        const yy = out.getFullYear();
        const MM = String(out.getMonth() + 1).padStart(2, '0');
        const dd = String(out.getDate()).padStart(2, '0');
        const hh = String(out.getHours()).padStart(2, '0');
        const mm = String(out.getMinutes()).padStart(2, '0');
        const ss = String(out.getSeconds()).padStart(2, '0');
        return `${yy}-${MM}-${dd}T${hh}:${mm}:${ss}`;
    }
    return out.toISOString();
}

/** 'YYYY-MM-DD' + N 일. all-day 일정 end 계산용. */
export function _addDaysDate(yyyymmdd: string, days: number): string {
    const m = yyyymmdd.match(/^(\d{4})-(\d{2})-(\d{2})$/);
    if (!m) return yyyymmdd;
    const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
    d.setDate(d.getDate() + days);
    const yy = d.getFullYear(), MM = String(d.getMonth() + 1).padStart(2, '0'), dd = String(d.getDate()).padStart(2, '0');
    return `${yy}-${MM}-${dd}`;
}

/**
 * 현재 access token 이 유효하면 그대로, 아니면 refresh. config 의 만료 시각 사용.
 * 갱신 후 만료 시각도 config 에 기록 — 다음 호출 때 불필요한 갱신 방지.
 *
 * Calendar / Sheets API 양쪽이 같은 token 을 공유한다 (scope 가 모두 같은 OAuth 에 포함).
 * 그래서 `_` prefix 떼고 export — Sheets API client 가 직접 호출.
 */
export async function getFreshAccessToken(
    context: vscode.ExtensionContext,
): Promise<{ ok: true; accessToken: string } | { ok: false; error: string }> {
    const cfg = readCalendarConfig(context);
    if (!cfg.clientId || !cfg.clientSecret || !cfg.refreshToken) {
        return { ok: false, error: 'Google OAuth 가 설정되지 않았습니다. "Astra: Google Calendar OAuth 연결 (쓰기)" 명령으로 한 번 로그인하세요. (Calendar 와 Sheets 권한이 함께 발급됩니다.)' };
    }
    const now = Date.now();
    if (cfg.accessToken && cfg.accessTokenExpiresAt && cfg.accessTokenExpiresAt > now) {
        return { ok: true, accessToken: cfg.accessToken };
    }
    const r = await refreshAccessToken(cfg.clientId, cfg.clientSecret, cfg.refreshToken);
    if (!r.ok) {
        // 리프레시 토큰 자체가 만료/철회된 경우 — raw 구글 에러("Token has been
        // expired or revoked.")만 보여주면 사용자가 복구 방법을 알 수 없다.
        // 재연결 명령 + 흔한 근본 원인(OAuth 동의 화면 '테스트' 모드 = 7일 만료) 안내.
        if (/expired|revoked|invalid_grant/i.test(r.error)) {
            return {
                ok: false,
                error: 'Google OAuth 토큰이 만료/철회되었습니다 — "Astra: Google Calendar OAuth 연결 (쓰기)" 명령으로 재연결하세요. '
                    + "(자주 반복되면: Google Cloud Console → OAuth 동의 화면이 '테스트' 모드인지 확인 — 테스트 모드는 리프레시 토큰이 7일마다 만료되며, '앱 게시'로 프로덕션 전환 시 만료되지 않습니다.) "
                    + `원인: ${r.error}`,
            };
        }
        return { ok: false, error: r.error };
    }
    await writeCalendarConfig(context, { accessToken: r.accessToken, accessTokenExpiresAt: r.expiresAt });
    return { ok: true, accessToken: r.accessToken };
}

// 내부 호출용 alias 유지 — 한 줄짜리라 비용 없음.
const _getFreshAccessToken = getFreshAccessToken;
