package handlers

// Submission generator HTTP layer (t-paliad-230 — format-only scope
// reduction of t-paliad-215; t-paliad-242 broadened the list endpoint
// to the full cross-proceeding catalog; t-paliad-253 promoted /generate
// from format-only to the same merge engine the draft editor uses).
//
// Endpoints:
//
//	GET  /api/projects/{id}/submissions
//	       Lists every published filing rule across every active
//	       proceeding the platform knows about, joined with its
//	       proceeding_type so the frontend can group by proceeding.
//	       has_template flips per-row: true when a per-submission .docx
//	       is wired in submissionTemplateRegistry, false when the
//	       editor falls back to the universal HL Patents Style.
//
//	POST /api/projects/{id}/submissions/{code}/generate
//	       Resolves the template through the cronus fallback chain
//	       (per-firm `submissionTemplateRegistry[code]` first, HL
//	       Patents Style as the universal fallback), builds a fresh
//	       variable bag via SubmissionVarsService.Build, and runs the
//	       SubmissionRenderer merge so every {{placeholder}} resolves
//	       to project state (or `[KEIN WERT: key]` for empties). Writes
//	       one paliad.system_audit_log row and streams the .docx as an
//	       attachment download. The HL Patents Style fallback has no
//	       placeholders today, so for codes without a per-firm template
//	       the renderer is a no-op on substitution but still runs the
//	       .dotm→.docx pre-pass.
//
// Visibility: every endpoint runs through ProjectService.GetByID
// (paliad.can_see_project gate). Unauthorised callers get 404 — same
// convention as the rest of the project surfaces (no project-existence
// enumeration).

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"

	"mgit.msbls.de/m/paliad/internal/models"
	"mgit.msbls.de/m/paliad/internal/services"
)

// submissionRenderTimeout caps a single generate request. .dotm fetch
// is from the in-process cache (sub-millisecond) and the convert step
// is a single zip round-trip; the timeout exists so a cold cache miss
// against Gitea surfaces quickly rather than letting the browser spin.
const submissionRenderTimeout = 30 * time.Second

// docxMime is the .docx Content-Type per the OOXML spec.
const docxMime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"

// hlPatentsStyleSlug names the universal style template inside the
// fileRegistry in files.go. Both surfaces (the /files download for
// Word's auto-update channel and this generator) share the same
// cache entry so a refresh through one path is visible to the other.
const hlPatentsStyleSlug = "hl-patents-style.dotm"

// submissionListEntry is one row in the Schriftsätze panel.
type submissionListEntry struct {
	SubmissionCode   string `json:"submission_code"`
	Name             string `json:"name"`
	NameEN           string `json:"name_en"`
	EventType        string `json:"event_type,omitempty"`
	PrimaryParty     string `json:"primary_party,omitempty"`
	LegalSource      string `json:"legal_source,omitempty"`
	HasTemplate      bool   `json:"has_template"`
	ProceedingCode   string `json:"proceeding_code"`
	ProceedingName   string `json:"proceeding_name"`
	ProceedingNameEN string `json:"proceeding_name_en"`
}

// submissionListResponse wraps the list with a project-level header.
//
// ProjectProceedingCode names the project's own proceeding so the
// frontend can pin its group to the top of the grouped catalog
// (t-paliad-242). nil when the project hasn't bound a proceeding yet.
type submissionListResponse struct {
	ProjectID             uuid.UUID             `json:"project_id"`
	ProceedingTypeID      *int                  `json:"proceeding_type_id,omitempty"`
	ProjectProceedingCode *string               `json:"project_proceeding_code,omitempty"`
	Entries               []submissionListEntry `json:"entries"`
}

// handleListProjectSubmissions returns every published filing rule
// across every active proceeding the platform knows about, joined with
// its proceeding_type so the Schriftsätze tab can group rows by
// proceeding (t-paliad-242 — m wants to see the entire catalog from any
// project, not just the rules for the project's own proceeding).
//
// Visibility is gated on the PROJECT (paliad.can_see_project via
// ProjectService.GetByID); the rules themselves are static reference
// data shared across the firm.
//
// has_template flips when a per-submission .docx is wired into
// submissionTemplateRegistry (files.go). When false, the universal HL
// Patents Style .dotm is the fallback — the editor (t-paliad-238)
// resolves both flavours transparently, so every row remains
// generatable and editable from the UI.
//
// Rows are sorted by (proceeding_code, submission_code) so the
// frontend's groupBy stays cheap and the order is stable.
func handleListProjectSubmissions(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	projectID, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid project id"})
		return
	}

	ctx := r.Context()

	project, err := dbSvc.projects.GetByID(ctx, uid, projectID)
	if err != nil {
		writeServiceError(w, err)
		return
	}

	resp := submissionListResponse{
		ProjectID:        projectID,
		ProceedingTypeID: project.ProceedingTypeID,
		Entries:          []submissionListEntry{},
	}

	entries, ownCode, err := loadSubmissionCatalog(ctx, project.ProceedingTypeID)
	if err != nil {
		log.Printf("submissions: list submission catalog: %v", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "rule lookup failed"})
		return
	}
	resp.Entries = entries
	resp.ProjectProceedingCode = ownCode

	writeJSON(w, http.StatusOK, resp)
}

// handleListSubmissionCatalog returns the same cross-proceeding catalog
// without a project context — used by the global /submissions/new
// picker (t-paliad-243). No project_proceeding_code is returned since
// the picker isn't pinned to one project.
func handleListSubmissionCatalog(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	if _, ok := requireUser(w, r); !ok {
		return
	}

	entries, _, err := loadSubmissionCatalog(r.Context(), nil)
	if err != nil {
		log.Printf("submissions: list global submission catalog: %v", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "rule lookup failed"})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"entries": entries})
}

// loadSubmissionCatalog runs the shared catalog query. When
// projectProceedingTypeID is non-nil, the returned ownCode points at
// that proceeding's code so the frontend can pin its group to the top;
// otherwise ownCode is nil.
func loadSubmissionCatalog(ctx context.Context, projectProceedingTypeID *int) ([]submissionListEntry, *string, error) {
	type catalogRow struct {
		SubmissionCode   string  `db:"submission_code"`
		Name             string  `db:"name"`
		NameEN           string  `db:"name_en"`
		EventType        *string `db:"event_type"`
		PrimaryParty     *string `db:"primary_party"`
		LegalSource      *string `db:"legal_source"`
		ProceedingID     int     `db:"proceeding_type_id"`
		ProceedingCode   string  `db:"proceeding_code"`
		ProceedingName   string  `db:"proceeding_name"`
		ProceedingNameEN string  `db:"proceeding_name_en"`
	}

	var rows []catalogRow
	err := dbSvc.projects.DB().SelectContext(ctx, &rows,
		`SELECT dr.submission_code     AS submission_code,
		        dr.name                AS name,
		        dr.name_en             AS name_en,
		        dr.event_type          AS event_type,
		        dr.primary_party       AS primary_party,
		        dr.legal_source        AS legal_source,
		        dr.proceeding_type_id  AS proceeding_type_id,
		        pt.code                AS proceeding_code,
		        pt.name                AS proceeding_name,
		        pt.name_en             AS proceeding_name_en
		   FROM paliad.deadline_rules_unified dr
		   JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
		  WHERE dr.is_active = true
		    AND dr.lifecycle_state = 'published'
		    AND dr.event_type = 'filing'
		    AND dr.submission_code IS NOT NULL
		    AND dr.submission_code <> ''
		    AND pt.is_active = true
		  ORDER BY pt.code ASC, dr.sequence_order ASC, dr.submission_code ASC`)
	if err != nil {
		return nil, nil, err
	}

	entries := make([]submissionListEntry, 0, len(rows))
	var ownCode *string
	for _, row := range rows {
		entry := submissionListEntry{
			SubmissionCode:   row.SubmissionCode,
			Name:             row.Name,
			NameEN:           row.NameEN,
			HasTemplate:      hasPerSubmissionTemplate(row.SubmissionCode),
			ProceedingCode:   row.ProceedingCode,
			ProceedingName:   row.ProceedingName,
			ProceedingNameEN: row.ProceedingNameEN,
		}
		if row.EventType != nil {
			entry.EventType = *row.EventType
		}
		if row.PrimaryParty != nil {
			entry.PrimaryParty = *row.PrimaryParty
		}
		if row.LegalSource != nil {
			entry.LegalSource = *row.LegalSource
		}
		entries = append(entries, entry)
		if projectProceedingTypeID != nil && row.ProceedingID == *projectProceedingTypeID && ownCode == nil {
			code := row.ProceedingCode
			ownCode = &code
		}
	}

	// If the project's proceeding has no filing rules of its own, fall
	// back to a direct proceeding_types lookup so the frontend can still
	// pin the right group even when the catalog ordering wouldn't have
	// surfaced the code via a row.
	if projectProceedingTypeID != nil && ownCode == nil {
		var code string
		if err := dbSvc.projects.DB().GetContext(ctx, &code,
			`SELECT code FROM paliad.proceeding_types WHERE id = $1`, *projectProceedingTypeID); err == nil && code != "" {
			ownCode = &code
		}
	}

	return entries, ownCode, nil
}

// hasPerSubmissionTemplate reports whether a per-submission .docx is
// wired in the fileRegistry (files.go). false means the editor falls
// back to the universal HL Patents Style — still renderable, still
// editable, but the UI may want to surface a "universal Vorlage"
// indicator. Read-only — no I/O, just a map lookup.
func hasPerSubmissionTemplate(submissionCode string) bool {
	_, ok := submissionTemplateRegistry[submissionCode]
	return ok
}

// handleGenerateProjectSubmission resolves the per-submission template
// (per-firm first, HL Patents Style fallback), builds a fresh variable
// bag from project state via SubmissionVarsService, runs the merge
// engine so every {{placeholder}} substitutes, writes one audit row,
// and streams the result. Pre-t-paliad-253 this handler ignored the
// per-firm registry and returned the bare HL Patents Style .dotm with
// no substitution — the "Generieren" button on the Schriftsätze tab
// therefore produced a generic firm-style .docx instead of a
// project-merged Klageerwiderung, which is what m noticed in
// m/paliad#84.
func handleGenerateProjectSubmission(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if dbSvc.submissionDraft == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "submissions not configured",
		})
		return
	}
	projectID, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid project id"})
		return
	}
	submissionCode := strings.TrimSpace(r.PathValue("code"))
	if submissionCode == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "submission code required"})
		return
	}

	ctx, cancel := context.WithTimeout(r.Context(), submissionRenderTimeout)
	defer cancel()

	// One-shot /generate has no draft row to pull `language` from —
	// accept `?language=de|en` as an explicit override (t-paliad-276)
	// and otherwise fall back to the user's UI language.
	user, _ := dbSvc.users.GetByID(ctx, uid)
	lang := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("language")))
	if lang != "de" && lang != "en" {
		lang = userLang(user)
	}

	tplBytes, _, _, err := resolveSubmissionTemplate(ctx, submissionCode, lang)
	if err != nil {
		log.Printf("submissions: template fetch (project=%s code=%s): %v", projectID, submissionCode, err)
		writeJSON(w, http.StatusBadGateway, map[string]string{"error": "template upstream unreachable"})
		return
	}

	docx, resolved, err := dbSvc.submissionDraft.RenderProjectSubmission(ctx, uid, projectID, submissionCode, lang, tplBytes)
	if err != nil {
		if errors.Is(err, services.ErrSubmissionRuleNotFound) {
			writeJSON(w, http.StatusNotFound, map[string]string{
				"error": fmt.Sprintf("no published rule for submission_code %q", submissionCode),
			})
			return
		}
		// ErrNotVisible / project ErrNotFound from the visibility gate
		// surface through writeServiceError as 404, matching the rest
		// of the project surfaces.
		log.Printf("submissions: render (project=%s code=%s): %v", projectID, submissionCode, err)
		writeServiceError(w, err)
		return
	}

	// One-click /generate has no saved draft row → no override store, so
	// the keyword stays the auto-derived rule name (t-paliad-354).
	filename := submissionFileName(resolved.Rule, resolved.Project, resolved.Lang, "")

	// Audit write is best-effort with a background context so the
	// download still succeeds if the DB races. Audit failure here only
	// affects the system_audit_log feed — never the user's response.
	bgCtx, cancelBG := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancelBG()
	if err := writeSubmissionAuditRow(bgCtx, resolved.User, projectID, submissionCode, resolved.Rule.Name, filename); err != nil {
		log.Printf("submissions: audit insert failed (project=%s code=%s): %v", projectID, submissionCode, err)
	}

	w.Header().Set("Content-Type", docxMime)
	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%q`, filename))
	w.Header().Set("Content-Length", strconv.Itoa(len(docx)))
	if _, err := w.Write(docx); err != nil {
		log.Printf("submissions: response write failed (project=%s code=%s): %v", projectID, submissionCode, err)
	}
}

// submissionFileName produces the user-facing download name
// (t-paliad-354): "<YYYY-MM-DD> <keyword> (<case number>).docx". The scheme
// is now the submission_docx_filename artifact of the pkg/nomen engine; this
// remains a thin wrapper so the call-sites and regression tests stay put.
// See services.RenderSubmissionFilename (internal/services/namegen.go).
func submissionFileName(rule *models.DeadlineRule, project *models.Project, lang, keyword string) string {
	return services.RenderSubmissionFilename(rule, project, lang, keyword)
}

// submissionFilenameKeyword pulls the user's filename keyword override
// from a saved draft's composer_meta jsonb (t-paliad-354). Empty when the
// key is absent or blank — callers then fall back to the auto-derived rule
// name inside submissionFileName. The one-click /generate path has no draft
// row and always passes "".
func submissionFilenameKeyword(d *services.SubmissionDraft) string {
	if d == nil || d.ComposerMeta == nil {
		return ""
	}
	if v, ok := d.ComposerMeta["filename_keyword"].(string); ok {
		return strings.TrimSpace(v)
	}
	return ""
}

// writeSubmissionAuditRow files one row in paliad.system_audit_log per
// generation. event_type='submission.generated', scope='project',
// scope_root=project_id. Metadata is intentionally small per Slice 1:
// {submission_code, rule_name, filename} — enough for a reviewer to
// reconstruct which template was offered to which project without
// over-baking the audit shape.
func writeSubmissionAuditRow(ctx context.Context, user *models.User, projectID uuid.UUID, submissionCode, ruleName, filename string) error {
	meta := map[string]any{
		"submission_code": submissionCode,
		"rule_name":       ruleName,
		"filename":        filename,
	}
	body, _ := json.Marshal(meta)
	var (
		actorID    any
		actorEmail string
	)
	if user != nil {
		actorID = user.ID
		actorEmail = user.Email
	}
	_, err := dbSvc.projects.DB().ExecContext(ctx,
		`INSERT INTO paliad.system_audit_log
		    (event_type, actor_id, actor_email, scope, scope_root, metadata)
		 VALUES ('submission.generated', $1, $2, 'project', $3, $4::jsonb)`,
		actorID, actorEmail, projectID.String(), string(body),
	)
	return err
}
