<script setup>
import { ref, computed, watch, onMounted } from "vue";
import Message from "primevue/message";
import BaseIndex from "../../../components/base/BaseIndex.vue";
import HoursToolbar from "../components/HoursToolbar.vue";
import HoursWeekStrip from "../components/HoursWeekStrip.vue";
import HoursQuickEntryBar from "../components/HoursQuickEntryBar.vue";
import WeekGridView from "../components/WeekGridView.vue";
import DayView from "../components/DayView.vue";
import MonthOverviewView from "../components/MonthOverviewView.vue";
import HoursListView from "../components/HoursListView.vue";
import { useToast } from "primevue/usetoast";
import { apiErrorDetail } from "../../../services/api.js";
import {
  WEEKDAYS as WEEKDAY_KEYS,
  fetchCurrentWeek,
  syncWeekLines,
  submitWeek as submitWeekApi,
  fetchRegistry,
  fetchWorkbenchDailyTotals,
  fetchTasks,
} from "../composables/useHoursApi.js";
import {
  todayIsoLocal,
  mondayIsoForLocalDate,
  addCalendarDaysIso,
  shiftWeekKeepingOffset,
  weekdayKeyFromMonday,
  isoWeekNumber,
  monthFirstIso,
  addMonthsIso,
  daysInMonthYm,
} from "../composables/useHourWeekNavigate.js";
import strings from "../strings.js";

const STORAGE_MODE_KEY = "maestro.hours.workbench.mode";

const toast = useToast();
const loading = ref(true);
const saving = ref(false);
const week = ref(/** @type {Record<string, unknown> | null} */ (null));
const lines = ref(/** @type {Array<Record<string, unknown>>} */ ([]));
const warnings = ref([]);
const dismissedWarn = ref(/** @type {Record<string, boolean>} */ ({}));
const registry = ref({
  internalCategories: [],
  customers: [],
  projects: [],
});
const pendingRemovedIds = ref(/** @type {number[]} */ ([]));

const viewMode = ref(/** @type {"day" | "week" | "month" | "list"} */ ("day"));
const selectedIsoDate = ref(todayIsoLocal());
const monthAnchorFirst = ref(monthFirstIso(todayIsoLocal()));
const hoursByMonthDate = ref(/** @type {Record<string, number>} */ ({}));

function loadStoredMode() {
  try {
    const raw = sessionStorage.getItem(STORAGE_MODE_KEY);
    if (raw === "day" || raw === "week" || raw === "month" || raw === "list") {
      viewMode.value = raw;
    }
  } catch {
    /* ignore */
  }
}

const weekStartIso = computed(() =>
  typeof week.value?.week_start === "string" ? week.value.week_start : "",
);
const anchorMondayIso = computed(() => mondayIsoForLocalDate(selectedIsoDate.value));
const weekLocked = computed(() => week.value?.workflow_status === "approved");

const hasInternalCategories = computed(
  () =>
    Array.isArray(registry.value.internalCategories) &&
    registry.value.internalCategories.length > 0,
);
const hasCustomers = computed(
  () =>
    Array.isArray(registry.value.customers) && registry.value.customers.length > 0,
);
const hasProjects = computed(
  () =>
    Array.isArray(registry.value.projects) && registry.value.projects.length > 0,
);
const hasAnyHourTarget = computed(
  () =>
    hasInternalCategories.value ||
    hasCustomers.value ||
    hasProjects.value,
);
const registryHelpVisible = computed(() => !loading.value && !hasAnyHourTarget.value);
const visibleWarnings = computed(() =>
  warnings.value.filter((w) => !dismissedWarn.value[w]),
);

const localeDateFmt = new Intl.DateTimeFormat(undefined, {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric",
});

function formatIsoPretty(iso) {
  try {
    const [y, m, d] = iso.split("-").map(Number);
    return localeDateFmt.format(new Date(y, m - 1, d, 12, 0, 0));
  } catch {
    return iso;
  }
}

const dateTitle = computed(() => {
  const m = selectedIsoDate.value;
  switch (viewMode.value) {
    case "month": {
      const [y, mo] = monthAnchorFirst.value.split("-").map(Number);
      const head = new Date(y, mo - 1, 1);
      return `${strings.weeklyTitle} — ${head.toLocaleString(undefined, {
        month: "long",
        year: "numeric",
      })}`;
    }
    case "week":
      return `${strings.weeklyTitle} · ${strings.weekNumber} ${isoWeekNumber(anchorMondayIso.value)} · ${m}`;
    default:
      return `${strings.weeklyTitle} · ${formatIsoPretty(m)}`;
  }
});

function blankDurations() {
  return WEEKDAY_KEYS.reduce((acc, k) => ({ ...acc, [k]: 0 }), {});
}

function mapIncomingLine(ln) {
  const durations = {};
  WEEKDAY_KEYS.forEach((d) => {
    const v = ln?.durations?.[d] ?? ln?.[`duration_${d}`] ?? 0;
    durations[d] = typeof v === "number" ? v : Number(v) || 0;
  });
  return {
    ...ln,
    registration_type: ln.registration_type ?? "internal",
    internal_category_id: ln.internal_category_id ?? null,
    hour_project_id: ln.hour_project_id ?? null,
    customer_id: ln.customer_id ?? null,
    hour_task_id: ln.hour_task_id ?? null,
    task_hour_project_id: ln.task_hour_project_id ?? null,
    description: ln.description ?? "",
    durations,
    _uiProjectForTasks: ln._uiProjectForTasks ?? ln.task_hour_project_id ?? null,
    _taskPickList: Array.isArray(ln._taskPickList) ? ln._taskPickList : [],
    line_approval_status: ln.line_approval_status,
  };
}

async function hydrateSingleTaskLine(ln) {
  if (ln.registration_type !== "task") {
    return;
  }
  const pid =
    /** @type {number | null | undefined} */ (ln._uiProjectForTasks) ??
    /** @type {number | null | undefined} */ (ln.task_hour_project_id ?? null) ??
    /** @type {number | undefined} */ (registry.value.projects?.[0]?.id ?? null);

  if (pid == null) {
    ln._taskPickList = [];

    return;
  }
  ln._uiProjectForTasks = Number(pid);

  try {
    ln._taskPickList = await fetchTasks(Number(pid));
  } catch {
    ln._taskPickList = [];
  }
}

async function hydrateAllTaskLines() {
  for (const ln of lines.value) {
    await hydrateSingleTaskLine(ln);
  }
}

async function loadWeekIso(isoMaybe) {
  const res = await fetchCurrentWeek(isoMaybe ?? undefined);
  week.value = res;
  pendingRemovedIds.value = [];
  const raw = Array.isArray(res?.lines) ? res.lines : [];
  lines.value = raw.map((ln) => mapIncomingLine(ln));
  await hydrateAllTaskLines();
}

async function bootstrap() {
  loading.value = true;
  warnings.value = [];
  dismissedWarn.value = {};
  try {
    registry.value = await fetchRegistry();
    loadStoredMode();
    await loadWeekIso(anchorMondayIso.value);
    if (viewMode.value === "month") {
      monthAnchorFirst.value = monthFirstIso(selectedIsoDate.value);
      await refreshMonthTotals();
    }
  } catch (e) {
    toast.add({
      severity: "error",
      summary: strings.weeklyTitle,
      detail: apiErrorDetail(e),
      life: 7000,
    });
  } finally {
    loading.value = false;
  }
}

watch(anchorMondayIso, async (next, prev) => {
  if (next === prev) {
    return;
  }

  if (loading.value || week.value == null) {
    return;
  }

  if (weekStartIso.value === next) {
    return;
  }

  loading.value = true;
  warnings.value = [];
  dismissedWarn.value = {};
  try {
    await loadWeekIso(next);
  } catch (e) {
    toast.add({
      severity: "error",
      summary: strings.weeklyTitle,
      detail: apiErrorDetail(e),
      life: 7000,
    });
  } finally {
    loading.value = false;
  }
});

watch(viewMode, (v) => {
  try {
    sessionStorage.setItem(STORAGE_MODE_KEY, v);
  } catch {
    /* ignore */
  }
  if (v === "month") {
    monthAnchorFirst.value = monthFirstIso(selectedIsoDate.value);
    void refreshMonthTotals();
  }
});

watch(monthAnchorFirst, () => {
  if (viewMode.value === "month") {
    void refreshMonthTotals();
  }
});

async function refreshMonthTotals() {
  try {
    const first = monthAnchorFirst.value;
    const yv = Number(first.slice(0, 4));
    const mv = Number(first.slice(5, 7));
    const dim = daysInMonthYm(first);
    const toIso = `${String(yv).padStart(4, "0")}-${String(mv).padStart(2, "0")}-${String(dim).padStart(2, "0")}`;
    const rows = await fetchWorkbenchDailyTotals(first, toIso);
    /** @type {Record<string, number>} */
    const map = {};

    for (const row of rows) {
      if (row?.date != null && typeof row.date === "string") {
        map[row.date] = Number(row.hours) || 0;
      }
    }
    hoursByMonthDate.value = map;
  } catch {
    hoursByMonthDate.value = {};
  }
}

onMounted(() => {
  void bootstrap();
});

function normalizedLinesPayload() {
  return lines.value.map((ln) => {
    const description = typeof ln.description === "string" ? ln.description.trim() : "";
    /** @type {Record<string, unknown>} */
    const base = {
      id: ln.id ?? undefined,
      registration_type: ln.registration_type,
      internal_category_id: null,
      hour_project_id: null,
      customer_id: null,
      hour_task_id: null,
      description,
    };
    switch (ln.registration_type) {
      case "internal":
        base.internal_category_id = ln.internal_category_id;
        break;
      case "customer":
        base.customer_id = ln.customer_id;
        break;
      case "project":
        base.hour_project_id = ln.hour_project_id;
        break;
      case "task":
        base.hour_task_id = ln.hour_task_id;
        break;
      default:
        break;
    }
    WEEKDAY_KEYS.forEach((d) => {
      base[`duration_${d}`] = Number(ln.durations[d]) || 0;
    });

    return base;
  });
}

async function persistLines(okToast) {
  if (!week.value?.id) {
    return;
  }

  saving.value = true;

  try {
    await syncWeekLines(
      Number(week.value.id),
      normalizedLinesPayload(),
      [...pendingRemovedIds.value],
    );

    pendingRemovedIds.value = [];
    if (okToast) {
      toast.add({
        severity: "success",
        summary: strings.save,
        detail: "Saved.",
        life: 4000,
      });
    }
    await loadWeekIso(weekStartIso.value);
    if (viewMode.value === "month") {
      await refreshMonthTotals();
    }
  } catch (e) {
    toast.add({
      severity: "error",
      summary: strings.save,
      detail: apiErrorDetail(e),
      life: 7000,
    });
  } finally {
    saving.value = false;
  }
}

async function submitCurrentWeek() {
  if (!week.value?.id || weekLocked.value) {
    return;
  }

  saving.value = true;

  try {
    await persistLines(false);
    const result = await submitWeekApi(Number(week.value.id));
    warnings.value = Array.isArray(result.warnings) ? result.warnings : [];
    dismissedWarn.value = {};
    toast.add({
      severity: "success",
      summary: strings.submit,
      detail: "Week submitted.",
      life: 5200,
    });
    await loadWeekIso(weekStartIso.value);

    if (viewMode.value === "month") {
      await refreshMonthTotals();
    }
  } catch (e) {
    toast.add({
      severity: "error",
      summary: strings.submit,
      detail: apiErrorDetail(e),
      life: 7000,
    });
  } finally {
    saving.value = false;
  }
}

function onToolbarShiftWeek(delta) {
  selectedIsoDate.value = shiftWeekKeepingOffset(
    selectedIsoDate.value,
    anchorMondayIso.value,
    delta,
  );
}

function onToolbarShiftDay(delta) {
  selectedIsoDate.value = addCalendarDaysIso(selectedIsoDate.value, delta);
}

function onGotoToday() {
  selectedIsoDate.value = todayIsoLocal();
}

function onGotoMonthPrev() {
  monthAnchorFirst.value = addMonthsIso(monthAnchorFirst.value, -1);
  selectedIsoDate.value = monthAnchorFirst.value;
}

function onGotoMonthNext() {
  monthAnchorFirst.value = addMonthsIso(monthAnchorFirst.value, 1);
  selectedIsoDate.value = monthAnchorFirst.value;
}

function dismissWarning(key) {
  dismissedWarn.value = { ...dismissedWarn.value, [key]: true };
}

function removeRow(ix) {
  const ln = lines.value[ix];
  if (!ln || weekLocked.value) {
    return;
  }
  if (ln.id) {
    pendingRemovedIds.value.push(Number(ln.id));
  }
  lines.value.splice(ix, 1);
}

function addInternalRow() {
  if (!hasInternalCategories.value) {
    return;
  }
  const id = registry.value.internalCategories?.[0]?.id;
  if (!id) {
    return;
  }
  lines.value.push({
    registration_type: "internal",
    internal_category_id: id,
    hour_project_id: null,
    customer_id: null,
    hour_task_id: null,
    task_hour_project_id: null,
    description: "",
    durations: blankDurations(),
    _taskPickList: [],
  });
}

function addCustomerRow() {
  if (!hasCustomers.value) {
    return;
  }
  const id = registry.value.customers?.[0]?.id;
  if (!id) {
    return;
  }
  lines.value.push({
    registration_type: "customer",
    internal_category_id: null,
    hour_project_id: null,
    customer_id: id,
    hour_task_id: null,
    task_hour_project_id: null,
    description: "",
    durations: blankDurations(),
    _taskPickList: [],
  });
}

function addProjectRow() {
  if (!hasProjects.value) {
    return;
  }
  const id = registry.value.projects?.[0]?.id;
  if (!id) {
    return;
  }
  lines.value.push({
    registration_type: "project",
    hour_project_id: id,
    internal_category_id: null,
    customer_id: null,
    hour_task_id: null,
    task_hour_project_id: null,
    description: "",
    durations: blankDurations(),
    _taskPickList: [],
  });
}

async function addQuickEntry(payload) {
  if (!week.value?.id || weekLocked.value || !payload) {
    return;
  }

  const dow = weekdayKeyFromMonday(weekStartIso.value, selectedIsoDate.value);
  if (!dow) {
    toast.add({
      severity: "warn",
      summary: strings.weeklyTitle,
      detail: "Selected calendar day is outside the loaded week.",
      life: 5000,
    });

    return;
  }

  const dur = blankDurations();
  const hrs =
    typeof payload.hours === "number" ? Math.round(payload.hours * 100) / 100 : 0;
  dur[dow] = hrs;

  if (hrs <= 0) {
    return;
  }

  const desc = typeof payload.description === "string" ? payload.description.trim() : "";

  if (payload.registration_type === "internal") {
    lines.value.push({
      registration_type: "internal",
      internal_category_id: payload.internal_category_id,
      hour_project_id: null,
      customer_id: null,
      hour_task_id: null,
      task_hour_project_id: null,
      description: desc,
      durations: dur,
      _taskPickList: [],
    });
  } else if (payload.registration_type === "customer") {
    lines.value.push({
      registration_type: "customer",
      customer_id: payload.customer_id,
      internal_category_id: null,
      hour_project_id: null,
      hour_task_id: null,
      task_hour_project_id: null,
      description: desc,
      durations: dur,
      _taskPickList: [],
    });
  } else if (payload.registration_type === "project") {
    lines.value.push({
      registration_type: "project",
      hour_project_id: payload.hour_project_id,
      hour_task_id: null,
      internal_category_id: null,
      customer_id: null,
      task_hour_project_id: null,
      description: desc,
      durations: dur,
      _taskPickList: [],
    });
  } else if (payload.registration_type === "task") {
    lines.value.push({
      registration_type: "task",
      hour_project_id: null,
      hour_task_id: payload.hour_task_id,
      internal_category_id: null,
      customer_id: null,
      task_hour_project_id: null,
      description: desc,
      durations: dur,
      _taskPickList: [],
      _uiProjectForTasks:
        typeof payload._preset_project_ui === "number"
          ? payload._preset_project_ui
          : null,
    });
    await hydrateSingleTaskLine(lines.value[lines.value.length - 1]);
  }

  await persistLines(false);
}

function onMonthPickDay(iso) {
  selectedIsoDate.value = iso;
  viewMode.value = "day";
}
</script>

<template>
  <BaseIndex flush hide-header>
    <div class="flex flex-col gap-4 p-4 md:p-5 max-w-[100rem] mx-auto">
      <HoursToolbar
        v-model:view-mode="viewMode"
        v-model:selected-iso-date="selectedIsoDate"
        :date-title="dateTitle"
        :disable-nav="loading"
        @shift-week="onToolbarShiftWeek"
        @shift-day="onToolbarShiftDay"
        @goto-today="onGotoToday"
        @goto-month-prev="onGotoMonthPrev"
        @goto-month-next="onGotoMonthNext"
      />

      <div v-if="visibleWarnings.length" class="flex flex-column gap-2">
        <Message
          v-for="w in visibleWarnings"
          :key="w"
          severity="warn"
          closable
          @close="dismissWarning(w)"
        >
          {{ w }}
        </Message>
      </div>

      <HoursWeekStrip
        :lines="lines"
        :week-monday-iso="anchorMondayIso"
        :selected-iso-date="selectedIsoDate"
        @select-day="(iso) => { selectedIsoDate = iso; }"
      />

      <HoursQuickEntryBar
        :week-locked="!!weekLocked"
        :registry="registry"
        :has-internal-categories="hasInternalCategories"
        :has-customers="hasCustomers"
        :has-projects="hasProjects"
        @add-entry="addQuickEntry"
      />

      <DayView
        v-show="viewMode === 'day'"
        :selected-iso-date="selectedIsoDate"
        :week-monday-iso="anchorMondayIso"
        :lines="lines"
        :registry="registry"
      />

      <WeekGridView
        v-show="viewMode === 'week'"
        :loading="loading"
        :saving="saving"
        :week-locked="!!weekLocked"
        :registry="registry"
        :registry-help-visible="registryHelpVisible"
        :week="week"
        :lines="lines"
        @persist="persistLines"
        @submit="submitCurrentWeek"
        @remove="removeRow"
        @add-internal="addInternalRow"
        @add-customer="addCustomerRow"
        @add-project="addProjectRow"
      />

      <MonthOverviewView
        v-show="viewMode === 'month'"
        :month-first-iso="monthAnchorFirst"
        :hours-by-date="hoursByMonthDate"
        @pick-day="onMonthPickDay"
      />

      <HoursListView
        v-show="viewMode === 'list'"
        :week-monday-iso="weekStartIso"
        :lines="lines"
        :registry="registry"
      />
    </div>
  </BaseIndex>
</template>
