package handlers

// Submission generator HTTP layer (t-paliad-215 Slice 1).
//
// Endpoints:
//
//	GET  /api/projects/{id}/submissions
//	       Lists the project's proceeding-relevant submission codes
//	       and reports template availability for each. Powers the
//	       SubmissionsPanel on the project detail page.
//
//	GET  /api/projects/{id}/submissions/{code}/generate
//	       Renders the .docx and streams it as an attachment download.
//	       Writes one paliad.system_audit_log row and one
//	       paliad.project_events row per generation. No server-side
//	       binary persistence (design §3, m's Q3 pick).
//
// Visibility: every endpoint runs through ProjectService.GetByID
// (paliad.can_see_project gate). Unauthorised callers get 404, never
// 403 — same convention as the rest of the project surfaces (avoids
// 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/branding"
	"mgit.msbls.de/m/paliad/internal/services"
)

// submissionRenderer + registry + vars are package-level singletons
// wired by Register() once at boot. Stateless rendering + thread-safe
// caches inside the registry mean no per-request construction.
var (
	submissionRenderer *services.SubmissionRenderer
	submissionRegistry *services.TemplateRegistry
	submissionVars     *services.SubmissionVarsService
)

// submissionRenderTimeout caps a single generate request. Template
// fetch (cache-miss) + rendering of a typical pleading takes well
// under a second; the timeout exists to surface "Gitea is unreachable"
// 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"

// submissionListEntry is one row in the SubmissionsPanel.
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"`
}

// submissionListResponse wraps the list with a project-level header.
type submissionListResponse struct {
	ProjectID        uuid.UUID             `json:"project_id"`
	ProceedingTypeID *int                  `json:"proceeding_type_id,omitempty"`
	Entries          []submissionListEntry `json:"entries"`
}

// handleListProjectSubmissions returns the filing-type rules for the
// project's proceeding, annotated with template availability.
func handleListProjectSubmissions(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if !requireSubmissionsWired(w) {
		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{},
	}

	if project.ProceedingTypeID == nil {
		writeJSON(w, http.StatusOK, resp)
		return
	}

	rules, err := dbSvc.rules.List(ctx, project.ProceedingTypeID)
	if err != nil {
		log.Printf("submissions: list rules for proceeding %d: %v", *project.ProceedingTypeID, err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "rule lookup failed"})
		return
	}

	for _, rule := range rules {
		if rule.SubmissionCode == nil || *rule.SubmissionCode == "" {
			continue
		}
		if rule.EventType == nil || *rule.EventType != "filing" {
			// Hearings + decisions don't generate submissions. The
			// "Schriftsätze" panel only lists filings.
			continue
		}
		if rule.LifecycleState != "published" {
			continue
		}
		entry := submissionListEntry{
			SubmissionCode: *rule.SubmissionCode,
			Name:           rule.Name,
			NameEN:         rule.NameEN,
			HasTemplate:    submissionRegistry.HasTemplate(ctx, *rule.SubmissionCode),
		}
		if rule.EventType != nil {
			entry.EventType = *rule.EventType
		}
		if rule.PrimaryParty != nil {
			entry.PrimaryParty = *rule.PrimaryParty
		}
		if rule.LegalSource != nil {
			entry.LegalSource = *rule.LegalSource
		}
		resp.Entries = append(resp.Entries, entry)
	}

	writeJSON(w, http.StatusOK, resp)
}

// handleGenerateProjectSubmission renders the .docx and streams it
// back to the browser. Audits the generation; never persists the
// rendered bytes server-side.
func handleGenerateProjectSubmission(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if !requireSubmissionsWired(w) {
		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()

	varsResult, err := submissionVars.Build(ctx, services.SubmissionVarsContext{
		UserID:         uid,
		ProjectID:      projectID,
		SubmissionCode: submissionCode,
	})
	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
		}
		writeServiceError(w, err)
		return
	}

	tmpl, err := submissionRegistry.Resolve(ctx, submissionCode)
	if err != nil {
		if errors.Is(err, services.ErrNoTemplate) {
			writeJSON(w, http.StatusServiceUnavailable, map[string]string{
				"error": "no template available for this submission",
				"hint":  "ask an admin to upload a .docx template under templates/_base/ in mWorkRepo",
			})
			return
		}
		log.Printf("submissions: template resolve for %s: %v", submissionCode, err)
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "template repository unreachable",
		})
		return
	}

	missing := services.DefaultMissingMarker(varsResult.Lang)
	rendered, err := submissionRenderer.Render(tmpl.Bytes, varsResult.Placeholders, missing)
	if err != nil {
		log.Printf("submissions: render %s for project %s: %v", submissionCode, projectID, err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "render failed",
		})
		return
	}

	filename := submissionFileName(varsResult, projectID)

	// Audit + Verlauf writes. Best-effort with a background context so
	// the user still receives the download even if the audit insert
	// races a slow DB.
	bgCtx, cancelBG := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancelBG()
	if err := writeSubmissionAuditRow(bgCtx, varsResult, tmpl, submissionCode); err != nil {
		log.Printf("submissions: audit insert failed (project=%s code=%s): %v", projectID, submissionCode, err)
	}
	if err := writeSubmissionProjectEvent(bgCtx, varsResult, tmpl, submissionCode); err != nil {
		log.Printf("submissions: project_events insert failed (project=%s code=%s): %v", projectID, submissionCode, err)
	}
	if err := writeSubmissionDocumentRow(bgCtx, varsResult, tmpl, submissionCode); err != nil {
		log.Printf("submissions: documents 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(rendered)))
	w.Header().Set("X-Paliad-Template-Sha", tmpl.SHA)
	w.Header().Set("X-Paliad-Template-Tier", tmpl.FirmTier)
	if _, err := w.Write(rendered); err != nil {
		log.Printf("submissions: response write failed (project=%s code=%s): %v", projectID, submissionCode, err)
	}
}

// requireSubmissionsWired returns false (and writes 503) when the
// generator wasn't constructed at boot. Happens in DATABASE_URL-less
// deployments — knowledge-platform-only stacks don't ship the
// submission engine.
func requireSubmissionsWired(w http.ResponseWriter) bool {
	if submissionRenderer == nil || submissionRegistry == nil || submissionVars == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "submission generator not configured",
		})
		return false
	}
	return true
}

// submissionFileName builds the user-facing filename per design §7:
//
//	{rule.name}-{project.case_number}-{YYYY-MM-DD}.docx
//
// Slashes and backslashes in case_number sanitise to underscores so
// the file saves cleanly across Windows + macOS + Linux. Missing
// case_number falls back to an 8-hex-char stable id from the project
// UUID so the file still has a deterministic handle.
func submissionFileName(vars *services.SubmissionVarsResult, projectID uuid.UUID) string {
	day := time.Now()
	if loc, err := time.LoadLocation("Europe/Berlin"); err == nil {
		day = day.In(loc)
	}
	ruleName := strings.TrimSpace(vars.Rule.Name)
	if strings.EqualFold(vars.Lang, "en") {
		ruleName = strings.TrimSpace(vars.Rule.NameEN)
	}
	if ruleName == "" {
		ruleName = "submission"
	}
	caseNo := ""
	if vars.Project != nil && vars.Project.CaseNumber != nil {
		caseNo = strings.TrimSpace(*vars.Project.CaseNumber)
	}
	if caseNo == "" {
		caseNo = projectID.String()[:8]
	}
	caseNo = strings.ReplaceAll(caseNo, "/", "_")
	caseNo = strings.ReplaceAll(caseNo, `\`, "_")
	return fmt.Sprintf("%s-%s-%s.docx", ruleName, caseNo, day.Format("2006-01-02"))
}

// writeSubmissionAuditRow files the org-wide audit entry. Reuses the
// system_audit_log convention (event_type='submission.generated')
// established in t-paliad-214's mig 102.
func writeSubmissionAuditRow(ctx context.Context, vars *services.SubmissionVarsResult, tmpl *services.ResolvedTemplate, code string) error {
	meta := map[string]any{
		"submission_code": code,
		"template_path":   tmpl.Path,
		"template_sha":    tmpl.SHA,
		"template_tier":   tmpl.FirmTier,
		"project_id":      vars.Project.ID.String(),
		"rule_id":         vars.Rule.ID.String(),
		"firm":            branding.Name,
	}
	body, _ := json.Marshal(meta)
	_, 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)`,
		vars.User.ID, vars.User.Email, vars.Project.ID.String(), string(body),
	)
	return err
}

// writeSubmissionProjectEvent surfaces the generation in the project
// Verlauf / SmartTimeline. event_type stays free-text (no CHECK on
// paliad.project_events.event_type per Slice 2 of SmartTimeline) so we
// don't need a migration to introduce 'submission_generated'.
func writeSubmissionProjectEvent(ctx context.Context, vars *services.SubmissionVarsResult, tmpl *services.ResolvedTemplate, code string) error {
	ruleName := strings.TrimSpace(vars.Rule.Name)
	if strings.EqualFold(vars.Lang, "en") {
		ruleName = strings.TrimSpace(vars.Rule.NameEN)
	}
	title := fmt.Sprintf("%s generiert", ruleName)
	if strings.EqualFold(vars.Lang, "en") {
		title = fmt.Sprintf("%s generated", ruleName)
	}
	meta := map[string]any{
		"submission_code": code,
		"template_path":   tmpl.Path,
		"template_sha":    tmpl.SHA,
		"template_tier":   tmpl.FirmTier,
		"rule_id":         vars.Rule.ID.String(),
	}
	body, _ := json.Marshal(meta)
	now := time.Now().UTC()
	_, err := dbSvc.projects.DB().ExecContext(ctx,
		`INSERT INTO paliad.project_events
		    (id, project_id, event_type, title, description, event_date,
		     created_by, metadata, created_at, updated_at)
		 VALUES ($1, $2, 'submission_generated', $3, NULL, $4, $5, $6::jsonb, $4, $4)`,
		uuid.New(), vars.Project.ID, title, now, vars.User.ID, string(body),
	)
	return err
}

// writeSubmissionDocumentRow files the audit-only paliad.documents
// row. file_path stays NULL — the bytes are regenerable from inputs
// (m's Q3 pick: no server-side binary). doc_type='generated_submission'
// is the additive marker; no CHECK constraint exists on doc_type, so
// this requires no migration.
func writeSubmissionDocumentRow(ctx context.Context, vars *services.SubmissionVarsResult, tmpl *services.ResolvedTemplate, code string) error {
	ruleName := strings.TrimSpace(vars.Rule.Name)
	if strings.EqualFold(vars.Lang, "en") {
		ruleName = strings.TrimSpace(vars.Rule.NameEN)
	}
	day := time.Now()
	if loc, err := time.LoadLocation("Europe/Berlin"); err == nil {
		day = day.In(loc)
	}
	title := fmt.Sprintf("%s (generiert %s)", ruleName, day.Format("2006-01-02"))
	if strings.EqualFold(vars.Lang, "en") {
		title = fmt.Sprintf("%s (generated %s)", ruleName, day.Format("2006-01-02"))
	}
	provenance := map[string]any{
		"submission_code": code,
		"template_path":   tmpl.Path,
		"template_sha":    tmpl.SHA,
		"template_tier":   tmpl.FirmTier,
		"firm":            branding.Name,
		"rule_id":         vars.Rule.ID.String(),
	}
	body, _ := json.Marshal(provenance)
	_, err := dbSvc.projects.DB().ExecContext(ctx,
		`INSERT INTO paliad.documents
		    (id, project_id, title, doc_type, file_path, file_size, mime_type,
		     ai_extracted, uploaded_by, created_at, updated_at)
		 VALUES ($1, $2, $3, 'generated_submission', NULL, NULL, $4, $5::jsonb, $6, now(), now())`,
		uuid.New(), vars.Project.ID, title, docxMime, string(body), vars.User.ID,
	)
	return err
}
