package services

// Submission variable bag — builds the PlaceholderMap that
// SubmissionRenderer fills into a template (t-paliad-215, design doc
// docs/design-submission-generator-2026-05-19.md §6.2).
//
// Variables span six namespaces:
//
//	firm.*       process-wide (branding.Name)
//	user.*       caller's user row
//	today.*      server time in Europe/Berlin, locale-aware
//	project.*    paliad.projects + joined proceeding type
//	parties.*    paliad.parties grouped by role
//	rule.*       paliad.deadline_rules row keyed by submission_code
//	deadline.*   next open paliad.deadlines row for (project, rule), if any
//
// Locale handling: every long-form date string is computed in both DE
// and EN; the renderer picks based on the user's lang preference. The
// rule pretty-printer (legalSourcePretty) also has DE/EN variants.
//
// Visibility: caller passes userID; ProjectService.GetByID enforces
// paliad.can_see_project — unauthorised callers get the standard
// ErrNotFound before any variable construction runs.

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"regexp"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"

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

// SubmissionVarsService assembles the placeholder map.
type SubmissionVarsService struct {
	db       *sqlx.DB
	projects *ProjectService
	parties  *PartyService
	users    *UserService
}

// NewSubmissionVarsService wires the service.
func NewSubmissionVarsService(db *sqlx.DB, projects *ProjectService, parties *PartyService, users *UserService) *SubmissionVarsService {
	return &SubmissionVarsService{
		db:       db,
		projects: projects,
		parties:  parties,
		users:    users,
	}
}

// SubmissionVarsContext is the input bundle that produces a render.
type SubmissionVarsContext struct {
	UserID         uuid.UUID
	ProjectID      uuid.UUID
	SubmissionCode string
}

// SubmissionVarsResult bundles the placeholder map with the lookup
// values the handler needs for the audit row + file naming
// (rule.Name, project.case_number, etc.).
type SubmissionVarsResult struct {
	Placeholders PlaceholderMap

	// Resolved entities for audit + naming.
	User           *models.User
	Project        *models.Project
	Rule           *models.DeadlineRule
	ProceedingType *models.ProceedingType
	Parties        []models.Party
	NextDeadline   *models.Deadline

	// Lang is the user's UI language used to pick locale-aware values
	// during the build. Returned so the renderer can use the matching
	// missing-marker function.
	Lang string
}

// ErrSubmissionRuleNotFound is returned when no published deadline_rule
// matches the requested submission_code. Maps to 404 in the handler.
var ErrSubmissionRuleNotFound = errors.New("submission generator: no rule found for submission_code")

// Build resolves every entity and assembles the placeholder map.
func (s *SubmissionVarsService) Build(ctx context.Context, in SubmissionVarsContext) (*SubmissionVarsResult, error) {
	if s.projects == nil || s.users == nil {
		return nil, fmt.Errorf("submission vars: required services not wired")
	}

	user, err := s.users.GetByID(ctx, in.UserID)
	if err != nil {
		return nil, err
	}
	if user == nil {
		return nil, ErrNotVisible
	}

	// Visibility gate — GetByID returns ErrNotFound when the user
	// can't see the project, which is exactly the 404 the handler
	// wants to propagate.
	project, err := s.projects.GetByID(ctx, in.UserID, in.ProjectID)
	if err != nil {
		return nil, err
	}

	rule, err := s.loadPublishedRule(ctx, in.SubmissionCode)
	if err != nil {
		return nil, err
	}

	pt, err := s.loadProceedingType(ctx, project.ProceedingTypeID)
	if err != nil {
		return nil, err
	}

	parties, err := s.parties.ListForProject(ctx, in.UserID, in.ProjectID)
	if err != nil {
		return nil, err
	}

	next, err := s.nextOpenDeadline(ctx, in.ProjectID, rule.ID)
	if err != nil {
		return nil, err
	}

	lang := user.Lang
	if lang == "" {
		lang = "de"
	}
	bag := PlaceholderMap{}
	addFirmVars(bag)
	addTodayVars(bag, time.Now())
	addUserVars(bag, user)
	addProjectVars(bag, project, pt, lang)
	addPartyVars(bag, parties)
	addRuleVars(bag, rule, lang)
	addDeadlineVars(bag, next, project, lang)

	return &SubmissionVarsResult{
		Placeholders:   bag,
		User:           user,
		Project:        project,
		Rule:           rule,
		ProceedingType: pt,
		Parties:        parties,
		NextDeadline:   next,
		Lang:           lang,
	}, nil
}

// loadPublishedRule fetches the deadline_rule that owns the given
// submission_code. Restricts to lifecycle_state='published' so drafts
// never end up shaping a real submission.
func (s *SubmissionVarsService) loadPublishedRule(ctx context.Context, submissionCode string) (*models.DeadlineRule, error) {
	if submissionCode == "" {
		return nil, ErrSubmissionRuleNotFound
	}
	var rule models.DeadlineRule
	err := s.db.GetContext(ctx, &rule,
		`SELECT `+ruleColumns+`
		   FROM paliad.deadline_rules
		  WHERE submission_code = $1
		    AND lifecycle_state = 'published'
		    AND is_active = true
		  ORDER BY sequence_order
		  LIMIT 1`, submissionCode)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrSubmissionRuleNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("load rule by submission_code %q: %w", submissionCode, err)
	}
	return &rule, nil
}

// loadProceedingType fetches the proceeding type row for the project's
// proceeding_type_id. Tolerates a nil id (returns nil, nil) so projects
// without a bound proceeding still render a meaningful template — the
// {{project.proceeding.*}} placeholders just resolve to the missing
// marker.
func (s *SubmissionVarsService) loadProceedingType(ctx context.Context, id *int) (*models.ProceedingType, error) {
	if id == nil {
		return nil, nil
	}
	var pt models.ProceedingType
	err := s.db.GetContext(ctx, &pt,
		`SELECT `+proceedingTypeColumns+`
		   FROM paliad.proceeding_types
		  WHERE id = $1`, *id)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("load proceeding type %d: %w", *id, err)
	}
	return &pt, nil
}

// nextOpenDeadline finds the earliest pending paliad.deadlines row on
// the given project that maps to the chosen rule. Returns (nil, nil)
// when no matching deadline exists — common when the lawyer is drafting
// the submission before the system has computed its deadline row.
func (s *SubmissionVarsService) nextOpenDeadline(ctx context.Context, projectID, ruleID uuid.UUID) (*models.Deadline, error) {
	var d models.Deadline
	err := s.db.GetContext(ctx, &d,
		`SELECT id, project_id, title, description, due_date, original_due_date,
		        warning_date, source, rule_id, rule_code, status, completed_at,
		        caldav_uid, caldav_etag, notes, created_by, created_at, updated_at,
		        approval_status, pending_request_id, approved_by, approved_at
		   FROM paliad.deadlines
		  WHERE project_id = $1
		    AND rule_id = $2
		    AND status = 'pending'
		  ORDER BY due_date ASC
		  LIMIT 1`, projectID, ruleID)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("load next deadline (project=%s rule=%s): %w", projectID, ruleID, err)
	}
	return &d, nil
}

// addFirmVars populates the firm.* namespace.
func addFirmVars(bag PlaceholderMap) {
	bag["firm.name"] = branding.Name
	// firm.signature_block is reserved for Phase 2; emit empty so
	// templates that already reference it don't render the missing
	// marker (less noisy for the lawyer).
	bag["firm.signature_block"] = ""
}

// addTodayVars populates today.* in both DE and EN long forms. ISO
// short form is the default {{today}}.
func addTodayVars(bag PlaceholderMap, now time.Time) {
	loc, _ := time.LoadLocation("Europe/Berlin")
	if loc != nil {
		now = now.In(loc)
	}
	bag["today"] = now.Format("2006-01-02")
	bag["today.iso"] = now.Format("2006-01-02")
	bag["today.long_de"] = formatLongDateDE(now)
	bag["today.long_en"] = formatLongDateEN(now)
}

// addUserVars populates user.*.
func addUserVars(bag PlaceholderMap, u *models.User) {
	bag["user.display_name"] = u.DisplayName
	bag["user.email"] = u.Email
	bag["user.office"] = u.Office
}

// addProjectVars populates project.* — title / case_number / court /
// patent_number / dates / our_side / proceeding metadata.
func addProjectVars(bag PlaceholderMap, p *models.Project, pt *models.ProceedingType, lang string) {
	bag["project.title"] = p.Title
	bag["project.reference"] = derefString(p.Reference)
	bag["project.case_number"] = derefString(p.CaseNumber)
	bag["project.court"] = derefString(p.Court)
	bag["project.patent_number"] = derefString(p.PatentNumber)
	// project.patent_number_upc is the UPC-brief convention — kind code
	// parenthesised ("EP 1 234 567 (B1)") instead of the DE form
	// ("EP 1 234 567 B1"). Pure-function rewrite; pass-through when no
	// kind code is present so the lawyer's draft never sees a worse
	// number than the source value.
	bag["project.patent_number_upc"] = patentNumberUPC(derefString(p.PatentNumber))
	bag["project.filing_date"] = formatDatePtr(p.FilingDate, "2006-01-02")
	bag["project.grant_date"] = formatDatePtr(p.GrantDate, "2006-01-02")
	bag["project.our_side"] = derefString(p.OurSide)
	bag["project.our_side_de"] = ourSideDE(derefString(p.OurSide))
	bag["project.our_side_en"] = ourSideEN(derefString(p.OurSide))
	bag["project.instance_level"] = derefString(p.InstanceLevel)
	bag["project.client_number"] = derefString(p.ClientNumber)
	bag["project.matter_number"] = derefString(p.MatterNumber)
	if pt != nil {
		bag["project.proceeding.code"] = pt.Code
		if strings.EqualFold(lang, "en") {
			bag["project.proceeding.name"] = pt.NameEN
		} else {
			bag["project.proceeding.name"] = pt.Name
		}
		bag["project.proceeding.name_de"] = pt.Name
		bag["project.proceeding.name_en"] = pt.NameEN
	}
}

// addPartyVars populates parties.* using the first row of each role.
// Multi-claimant / multi-defendant suits use the first row in Slice 1
// per design §13.6; expanded grouping is Phase 2.
func addPartyVars(bag PlaceholderMap, parties []models.Party) {
	var claimant, defendant, other *models.Party
	for i := range parties {
		role := strings.ToLower(strings.TrimSpace(derefString(parties[i].Role)))
		switch role {
		case "claimant", "kläger", "klaeger":
			if claimant == nil {
				claimant = &parties[i]
			}
		case "defendant", "beklagter", "beklagte":
			if defendant == nil {
				defendant = &parties[i]
			}
		default:
			if other == nil {
				other = &parties[i]
			}
		}
	}
	if claimant != nil {
		bag["parties.claimant.name"] = claimant.Name
		bag["parties.claimant.representative"] = derefString(claimant.Representative)
	}
	if defendant != nil {
		bag["parties.defendant.name"] = defendant.Name
		bag["parties.defendant.representative"] = derefString(defendant.Representative)
	}
	if other != nil {
		bag["parties.other.name"] = other.Name
		bag["parties.other.representative"] = derefString(other.Representative)
	}
}

// addRuleVars populates rule.* — submission_code, name(_en),
// legal_source (+ pretty form), primary_party, event_type.
func addRuleVars(bag PlaceholderMap, r *models.DeadlineRule, lang string) {
	bag["rule.submission_code"] = derefString(r.SubmissionCode)
	if strings.EqualFold(lang, "en") {
		bag["rule.name"] = r.NameEN
	} else {
		bag["rule.name"] = r.Name
	}
	bag["rule.name_de"] = r.Name
	bag["rule.name_en"] = r.NameEN
	bag["rule.legal_source"] = derefString(r.LegalSource)
	bag["rule.legal_source_pretty"] = legalSourcePretty(derefString(r.LegalSource), lang)
	bag["rule.primary_party"] = derefString(r.PrimaryParty)
	bag["rule.event_type"] = derefString(r.EventType)
}

// addDeadlineVars populates deadline.* from the next pending row. When
// no row exists the values fall through to the missing marker — the
// lawyer sees [KEIN WERT: deadline.due_date] in Word and knows to fix.
func addDeadlineVars(bag PlaceholderMap, d *models.Deadline, p *models.Project, lang string) {
	if d == nil {
		return
	}
	bag["deadline.due_date"] = d.DueDate.Format("2006-01-02")
	bag["deadline.due_date_long_de"] = formatLongDateDE(d.DueDate)
	bag["deadline.due_date_long_en"] = formatLongDateEN(d.DueDate)
	if d.OriginalDueDate != nil {
		bag["deadline.original_due_date"] = d.OriginalDueDate.Format("2006-01-02")
	}
	// computed_from carries the human-readable anchor description
	// (e.g. "Klagezustellung am 14.05.2026 + 6 Wochen"). Notes is
	// the closest existing field — the calculator stores anchor
	// metadata there. If empty we leave the placeholder unresolved.
	if d.Notes != nil && strings.TrimSpace(*d.Notes) != "" {
		bag["deadline.computed_from"] = strings.TrimSpace(*d.Notes)
	}
	bag["deadline.title"] = d.Title
	bag["deadline.source"] = d.Source
	_ = p // reserved for future shape decisions where the deadline
	// var depends on project context.
	_ = lang
}

// derefString returns *s or "" when s is nil.
func derefString(s *string) string {
	if s == nil {
		return ""
	}
	return *s
}

// formatDatePtr formats a *time.Time, returning "" for nil.
func formatDatePtr(t *time.Time, layout string) string {
	if t == nil {
		return ""
	}
	return t.Format(layout)
}

// ourSideDE returns the German legal-prose form of an our_side value.
func ourSideDE(side string) string {
	switch strings.ToLower(side) {
	case "claimant":
		return "Klägerin"
	case "defendant":
		return "Beklagte"
	case "court":
		return "Gericht"
	case "both":
		return "Klägerin und Beklagte"
	}
	return ""
}

// ourSideEN returns the English legal-prose form of an our_side value.
func ourSideEN(side string) string {
	switch strings.ToLower(side) {
	case "claimant":
		return "Claimant"
	case "defendant":
		return "Defendant"
	case "court":
		return "Court"
	case "both":
		return "Claimant and Defendant"
	}
	return ""
}

// formatLongDateDE renders a date in the German long form
// ("19. Mai 2026"). Pure function for unit testing.
func formatLongDateDE(t time.Time) string {
	months := []string{
		"Januar", "Februar", "März", "April", "Mai", "Juni",
		"Juli", "August", "September", "Oktober", "November", "Dezember",
	}
	idx := int(t.Month()) - 1
	if idx < 0 || idx >= len(months) {
		return t.Format("2006-01-02")
	}
	return fmt.Sprintf("%d. %s %d", t.Day(), months[idx], t.Year())
}

// formatLongDateEN renders a date in the English long form
// ("19 May 2026").
func formatLongDateEN(t time.Time) string {
	return t.Format("2 January 2006")
}

// legalSourcePretty rewrites the shorthand stored on deadline_rules
// (DE.ZPO.276.1, UPC.RoP.23.1, …) into the form a lawyer would type
// in a brief ("§ 276 Abs. 1 ZPO", "Rule 23.1 RoP UPC"). Unknown
// prefixes pass through unchanged — preferring the raw shorthand over
// an incorrect prettification.
//
// Lang controls the language of connective words (Abs / Section,
// Regel / Rule, …). The pretty table covers the prefixes used by the
// 254 published rules in the corpus today; new prefixes default to
// pass-through and a follow-up CL extends the table.
func legalSourcePretty(src, lang string) string {
	src = strings.TrimSpace(src)
	if src == "" {
		return ""
	}
	parts := strings.Split(src, ".")
	en := strings.EqualFold(lang, "en")

	switch {
	case len(parts) == 4 && parts[0] == "DE" && parts[1] == "ZPO":
		if en {
			return fmt.Sprintf("Section %s(%s) ZPO", parts[2], parts[3])
		}
		return fmt.Sprintf("§ %s Abs. %s ZPO", parts[2], parts[3])
	case len(parts) == 3 && parts[0] == "DE" && parts[1] == "ZPO":
		if en {
			return fmt.Sprintf("Section %s ZPO", parts[2])
		}
		return fmt.Sprintf("§ %s ZPO", parts[2])
	case len(parts) == 4 && parts[0] == "UPC" && parts[1] == "RoP":
		if en {
			return fmt.Sprintf("Rule %s.%s RoP UPC", parts[2], parts[3])
		}
		return fmt.Sprintf("Regel %s.%s VerfO UPC", parts[2], parts[3])
	case len(parts) == 3 && parts[0] == "UPC" && parts[1] == "RoP":
		if en {
			return fmt.Sprintf("Rule %s RoP UPC", parts[2])
		}
		return fmt.Sprintf("Regel %s VerfO UPC", parts[2])
	case len(parts) >= 3 && parts[0] == "DE" && parts[1] == "PatG":
		if en {
			return fmt.Sprintf("Section %s PatG", parts[2])
		}
		return fmt.Sprintf("§ %s PatG", parts[2])
	case len(parts) == 2 && parts[0] == "EPC":
		if en {
			return fmt.Sprintf("Art. %s EPC", parts[1])
		}
		return fmt.Sprintf("Art. %s EPÜ", parts[1])
	}
	return src
}

// patentNumberKindCodeRegex matches a trailing kind code on a patent
// number: a whitespace-separated single uppercase letter followed by
// a single digit (B1, A1, A2, B2, B9, C1, T2, U1, …). Capturing
// groups split the base from the kind code so the formatter can
// parenthesise the kind without touching the rest of the number.
var patentNumberKindCodeRegex = regexp.MustCompile(`^(.*?)\s+([A-Z]\d)$`)

// patentNumberUPC reformats a patent number from the DE convention
// ("EP 1 234 567 B1") to the UPC-brief convention
// ("EP 1 234 567 (B1)"). The kind code is parenthesised; everything
// else is preserved verbatim. Numbers without a recognised trailing
// kind code pass through unchanged so a lawyer's draft never sees a
// number worse than the source value.
//
// Recognised inputs:
//
//	"EP 1 234 567 B1"          → "EP 1 234 567 (B1)"
//	"EP 4 056 049 A1"          → "EP 4 056 049 (A1)"
//	"DE 10 2020 123 456 A1"    → "DE 10 2020 123 456 (A1)"
//	"  EP 1 234 567 B1  "      → "EP 1 234 567 (B1)" (trimmed)
//
// Pass-through:
//
//	"EP 1 234 567"             → "EP 1 234 567"
//	"WO/2023/123456"           → "WO/2023/123456"  (no kind code shape)
//	""                         → ""
//
// Pure function; unit-tested in submission_vars_test.go.
func patentNumberUPC(s string) string {
	s = strings.TrimSpace(s)
	if s == "" {
		return ""
	}
	if m := patentNumberKindCodeRegex.FindStringSubmatch(s); m != nil {
		base := strings.TrimSpace(m[1])
		kind := m[2]
		if base == "" {
			return s
		}
		return base + " (" + kind + ")"
	}
	return s
}
