/**
 * Collective store — the tenant: which collective is active, its full local
 * state (CollectiveState = Bundle minus export metadata), and the bundle
 * lifecycle (create from template, import, export, seeds, delete).
 *
 * EXPLICIT imports only (no Nuxt auto-imports): the store must run under
 * plain vitest + createPinia, and the future FastAPI adapter swaps
 * data/persistence without touching any screen.
 *
 * Getter shortcuts (people, decisions, …) filter soft-deleted rows
 * (!archivedAt) — screens never see archived entities; sync keeps them.
 */

import { defineStore } from 'pinia'
import type {
  Advice,
  Assent,
  Circle,
  Clause,
  ClauseVersion,
  CollectiveSettings,
  CollectiveTemplate,
  Concern,
  Decision,
  Id,
  ISODate,
  Mandate,
  Objection,
  Person,
  Protocol,
  TextDoc,
  VoteSession,
  Vote,
} from '../types/domain'
import type { CollectiveState, ImportResult } from '../data/persistence'
import {
  deleteCollective,
  getActiveCollectiveId,
  importBundle,
  listCollectiveIds,
  loadState,
  saveStateDebounced,
  saveStateNow,
  setActiveCollectiveId,
  toBundle,
} from '../data/persistence'
import { hasConsentProtocol, resolveSettings } from '../engine'
import { buildTemplateBundle } from '../data/templates'
import type { TemplateId } from '../data/templates'

/** Re-exported for screens: the shape of the active collective's state. */
export type { CollectiveState } from '../data/persistence'

export interface CollectiveIndexEntry {
  id: Id
  slug: string
  name: string
  color: string
  icon: string
  template: CollectiveTemplate
}

export interface CreateFromTemplateOptions {
  name: string
  slug: string
  color: string
  icon: string
  meName: string
  memberNames: string[]
}

export type SeedName = 'duniter-g1' | 'atelier-du-canal'

interface CollectiveStoreState {
  current: CollectiveState | null
  activeId: Id | null
  index: CollectiveIndexEntry[]
  ready: boolean
}

function live<T extends { archivedAt?: ISODate }>(rows: T[] | undefined): T[] {
  return (rows ?? []).filter(row => !row.archivedAt)
}

export const useCollectiveStore = defineStore('collective', {
  state: (): CollectiveStoreState => ({
    current: null,
    activeId: null,
    index: [],
    ready: false,
  }),

  getters: {
    /** The local profile — v2 single-seat. */
    me(state): Person | null {
      return state.current?.people.find(p => p.isMe && !p.archivedAt) ?? null
    },

    /** Resolved Pact settings — the Pact IS the settings store. */
    settings(state): CollectiveSettings | null {
      if (!state.current) return null
      const pactDocId = state.current.collective.pactDocId
      // Cast: Vue's UnwrapRef over the recursive Json type (settingValue)
      // explodes TS instantiation depth; the runtime shape is identical.
      const pactClauses = state.current.clauses.filter(
        c => c.docId === pactDocId && !c.archivedAt,
      ) as unknown as Clause[]
      const versions = state.current.versions as unknown as ClauseVersion[]
      return resolveSettings(pactClauses, versions)
    },

    hasConsent(): boolean {
      const settings = this.settings
      return settings !== null && hasConsentProtocol(settings)
    },

    pactDoc(state): TextDoc | null {
      if (!state.current) return null
      return (
        state.current.docs.find(
          d => d.id === state.current!.collective.pactDocId && !d.archivedAt,
        ) ?? null
      )
    },

    // ── Non-archived shortcuts — screens never see archived rows ──
    people(state): Person[] { return live(state.current?.people) },
    circles(state): Circle[] { return live(state.current?.circles) },
    decisions(state): Decision[] { return live(state.current?.decisions) },
    mandates(state): Mandate[] { return live(state.current?.mandates) },
    docs(state): TextDoc[] { return live(state.current?.docs) },
    clauses(state): Clause[] { return live(state.current?.clauses) },
    versions(state): ClauseVersion[] {
      // Cast: same recursive-Json unwrap issue as the settings getter.
      return live(state.current?.versions as unknown as ClauseVersion[] | undefined)
    },
    protocols(state): Protocol[] { return live(state.current?.protocols) },
    sessions(state): VoteSession[] { return live(state.current?.sessions) },
    votes(state): Vote[] { return live(state.current?.votes) },
    concerns(state): Concern[] { return live(state.current?.concerns) },
    objections(state): Objection[] { return live(state.current?.objections) },
    advices(state): Advice[] { return live(state.current?.advices) },
    assents(state): Assent[] { return live(state.current?.assents) },
  },

  actions: {
    newId(): Id {
      return crypto.randomUUID()
    },

    now(): ISODate {
      return new Date().toISOString()
    },

    /** LWW clock: bump updatedAt, return the entity. */
    stamp<T extends { updatedAt: ISODate }>(entity: T): T {
      entity.updatedAt = this.now()
      return entity
    },

    /** Debounced save of the active collective — every mutation ends here. */
    persist(): void {
      if (this.current) saveStateDebounced(this.current as unknown as CollectiveState)
    },

    /** Load the index of local collectives + the active one. */
    async init(): Promise<void> {
      const ids = await listCollectiveIds()
      const index: CollectiveIndexEntry[] = []
      for (const id of ids) {
        const state = await loadState(id)
        if (!state) continue
        const c = state.collective
        index.push({
          id: c.id, slug: c.slug, name: c.name,
          color: c.color, icon: c.icon, template: c.template,
        })
      }
      this.index = index

      const activeId = await getActiveCollectiveId()
      if (activeId && ids.includes(activeId)) {
        await this.switchTo(activeId)
      }
      this.ready = true
    },

    async switchTo(id: Id): Promise<void> {
      const state = await loadState(id)
      if (!state) return
      // Cast: assigning into the reactive slot re-triggers the recursive-Json
      // UnwrapRef explosion (see the settings getter) — same shape at runtime.
      this.current = state as unknown as typeof this.current
      this.activeId = id
      await setActiveCollectiveId(id)
    },

    /** Create a collective from one of the seven templates — SAME path as an import. */
    async createFromTemplate(id: TemplateId, opts: CreateFromTemplateOptions): Promise<ImportResult> {
      const bundle = buildTemplateBundle(id, {
        ...opts,
        now: this.now(),
        newId: () => this.newId(),
      })
      return await this.importJson(JSON.stringify(bundle), true)
    },

    /**
     * Import a bundle (user file or seed). Collision on the collective id is
     * REFUSED with a French issue — never a silent overwrite, never an id
     * suffix (the id is the sync identity of the collective).
     */
    async importJson(json: string, asSeed = false): Promise<ImportResult> {
      const result = await importBundle(json, { asSeed })
      if (!result.state) return result
      if (result.collided) {
        return {
          issues: [
            ...result.issues,
            {
              level: 'error',
              message:
                'Ce collectif existe déjà sur cette machine — supprime-le d\'abord si tu veux le réimporter.',
            },
          ],
          collided: true,
        }
      }
      await saveStateNow(result.state)
      const c = result.state.collective
      this.index.push({
        id: c.id, slug: c.slug, name: c.name,
        color: c.color, icon: c.icon, template: c.template,
      })
      await this.switchTo(c.id)
      return result
    },

    /** Export the active collective as an indented schemaVersion-2 bundle. */
    exportJson(): string | null {
      if (!this.current) return null
      return JSON.stringify(toBundle(this.current as unknown as CollectiveState, this.now()), null, 2)
    },

    async removeCollective(id: Id): Promise<void> {
      await deleteCollective(id)
      this.index = this.index.filter(entry => entry.id !== id)
      if (this.activeId === id) {
        this.activeId = null
        this.current = null
      }
    },

    /**
     * Import a packaged seed (Duniter Ğ1 / Atelier du Canal) through the SAME
     * path as a user import. The JSON files are produced by separate tooling:
     * a missing file fails SOFTLY with a French issue, never a crash.
     */
    async loadSeed(name: SeedName): Promise<ImportResult> {
      let bundle: unknown
      try {
        bundle
          = name === 'duniter-g1'
            ? (await import('../data/seeds/duniter-g1.bundle.json')).default
            : (await import('../data/seeds/atelier-du-canal.bundle.json')).default
      } catch {
        return {
          issues: [
            {
              level: 'error',
              message: `Le jeu de démonstration « ${name} » n'est pas disponible dans cette version.`,
            },
          ],
          collided: false,
        }
      }
      return await this.importJson(JSON.stringify(bundle), true)
    },
  },
})
