package incident

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"regexp"
	"strings"
	"time"

	userv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	"github.com/tidbcloud/devops/entity"
)

type extractedPostMortemAction struct {
	Title            string   `json:"title"`
	Description      string   `json:"description"`
	OwnerEmails      []string `json:"owner_emails"`
	OwnerLarkUserIDs []string `json:"owner_lark_user_ids"`
	Deadline         string   `json:"deadline"`
	Type             string   `json:"type"`
	Priority         string   `json:"priority"`
	LinkedJiraIssue  string   `json:"linked_jira_issue"`
	Status           string   `json:"status"`
}

type extractedPostMortemTimelineLog struct {
	EventTime   string `json:"event_time"`
	Description string `json:"description"`
}

type importedPostMortemAction struct {
	Title                   string
	Action                  *entity.IncidentPostMortemToDoAction
	OriginalLinkedJiraIssue *string
}

func (s *incidentService) CreatePostMortemToDoActionsFromFeishuDoc(ctx context.Context, incidentId uint64, feishuURL string) ([]*entity.IncidentPostMortemToDoAction, error) {
	documentID, err := s.documentTokenFromFeishuURL(ctx, feishuURL)
	if err != nil {
		return nil, err
	}
	content, err := s.larkclient.Document.GetDocumentPlainText(ctx, documentID)
	if err != nil {
		return nil, fmt.Errorf("get feishu document content failed: %w", err)
	}
	if strings.TrimSpace(content) == "" {
		return nil, fmt.Errorf("feishu document content is empty")
	}
	if err := s.createPostMortemTimelineLogsFromFeishuDoc(ctx, incidentId, content); err != nil {
		s.Logger(ctx).Error(err, "import postmortem timeline logs failed, continue todo action import", "incident_id", incidentId, "feishu_url", feishuURL)
	}
	actionSection := extractActionItemsSection(content)
	actions, err := s.extractPostMortemActions(ctx, actionSection)
	if err != nil {
		return nil, err
	}

	created := make([]*entity.IncidentPostMortemToDoAction, 0, len(actions))
	importedActions := make([]importedPostMortemAction, 0, len(actions))
	for _, action := range actions {
		title := stripLarkUserMentions(action.Title)
		description := stripLarkUserMentions(action.Description)
		if title == "" {
			continue
		}
		deadline, err := parseExtractedDeadline(action.Deadline)
		if err != nil {
			s.Logger(ctx).Error(err, "ignore invalid extracted todo action deadline", "title", action.Title, "deadline", action.Deadline)
		}
		persons := s.resolveExtractedActionOwners(ctx, action)
		status := strings.TrimSpace(action.Status)
		originalLinkedJiraIssue := optionalTrimmedString(action.LinkedJiraIssue)
		createdAction, err := s.CreateIncidentPostMortemToDoActionItem(ctx, &CreateIncidentPostMortemActionParams{
			IncidentId:               incidentId,
			Persons:                  persons,
			Deadline:                 deadline,
			Title:                    title,
			Type:                     optionalTrimmedString(action.Type),
			Priority:                 optionalTrimmedString(action.Priority),
			Description:              description,
			LinkedJiraIssue:          originalLinkedJiraIssue,
			Status:                   optionalTrimmedString(status),
			CreateAssociatedLarkTask: false,
		})
		if err != nil {
			return created, fmt.Errorf("create todo action from feishu document failed, title %q: %w", action.Title, err)
		}
		created = append(created, createdAction)
		importedActions = append(importedActions, importedPostMortemAction{
			Title:                   action.Title,
			Action:                  createdAction,
			OriginalLinkedJiraIssue: originalLinkedJiraIssue,
		})
	}
	var errs []error
	for _, importedAction := range importedActions {
		if err := s.ensureImportedToDoActionTCOCPLink(ctx, importedAction.Action, feishuURL, importedAction.OriginalLinkedJiraIssue); err != nil {
			s.Logger(ctx).Error(err, "ensure todo action TCOCP link failed, continue", "incident_id", incidentId, "title", importedAction.Title, "todo_action_id", importedAction.Action.Id)
			errs = append(errs, fmt.Errorf("ensure todo action TCOCP link failed, title %q: %w", importedAction.Title, err))
		}
	}
	return created, errors.Join(errs...)
}

func (s *incidentService) createPostMortemTimelineLogsFromFeishuDoc(ctx context.Context, incidentId uint64, documentContent string) error {
	timelineContext := extractTimelineExtractionContext(documentContent)
	if strings.TrimSpace(timelineContext) == "" {
		s.Logger(ctx).Info("postmortem timeline section not found, skip timeline log import", "incident_id", incidentId)
		return nil
	}
	logs, err := s.extractPostMortemTimelineLogs(ctx, timelineContext)
	if err != nil {
		return err
	}
	s.Logger(ctx).Info("postmortem timeline logs extracted", "incident_id", incidentId, "count", len(logs))
	created := 0
	for _, logItem := range logs {
		if _, err := s.CreateIncidentLogItem(ctx, incidentId, logItem); err != nil {
			return fmt.Errorf("create incident timeline log from feishu document failed: %w", err)
		}
		created++
	}
	s.Logger(ctx).Info("postmortem timeline logs created", "incident_id", incidentId, "count", created)
	return nil
}

func (s *incidentService) documentTokenFromFeishuURL(ctx context.Context, raw string) (string, error) {
	parsed, err := url.Parse(strings.TrimSpace(raw))
	if err != nil {
		return "", fmt.Errorf("invalid feishu url: %w", err)
	}
	parts := strings.Split(strings.Trim(parsed.Path, "/"), "/")
	for i, part := range parts {
		if part == "docx" && i+1 < len(parts) {
			return parts[i+1], nil
		}
		if part == "wiki" && i+1 < len(parts) {
			return s.larkclient.Document.GetWikiDocumentToken(ctx, parts[i+1])
		}
	}
	return "", fmt.Errorf("feishu url must contain /docx/{token} or /wiki/{token}")
}

func (s *incidentService) extractPostMortemActions(ctx context.Context, documentContent string) ([]extractedPostMortemAction, error) {
	if s.llmCli == nil {
		return nil, fmt.Errorf("llm client is not configured")
	}
	llmCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
	defer cancel()

	resp, err := s.llmCli.GenerateText(llmCtx, buildPostMortemActionExtractionPrompt(documentContent))
	if err != nil {
		return nil, fmt.Errorf("extract todo actions with llm failed: %w", err)
	}
	actions, err := parseExtractedPostMortemActions(resp)
	if err != nil {
		return nil, err
	}
	return actions, nil
}

func buildPostMortemActionExtractionPrompt(documentContent string) string {
	return fmt.Sprintf(`You extract incident postmortem action items.

Read only the "Action Items" or "Action List" section from the Feishu postmortem document below. It may be a table rendered as plain text or XML-like <table>/<row>/<cell> text.
Return strict JSON only, with this shape:
[
  {
    "title": "short row-level action title",
    "description": "full Item cell content",
    "owner_emails": ["owner@example.com"],
    "owner_lark_user_ids": ["lark user id from Assignee cell only, if owner is shown as @lark_user_id:<id>"],
    "deadline": "YYYY-MM-DD or empty",
    "type": "one of Mitigation SOP, New alert/metric/log, Operation SOP, Healthcheck, ConfigChange",
    "priority": "original Priority cell value, e.g. P0, P1, P2, P3, empty if missing",
    "linked_jira_issue": "Jira key or URL if present, empty otherwise",
    "status": "open|in_progress|done or empty"
  }
]

Rules:
- If there is no Action Items or Action List section, return [].
- For an Action Items table, map cells by the header row. Common columns are Priority, Type, Deadline, Status, JIRA, Item, and Assignee.
- For an Action Items table, produce exactly one JSON object per data row.
- Do not split one table row because the Item cell contains multiple lines, bullets, section labels, or mentions.
- title should be a concise summary of the row's Item cell.
- description should preserve the full row-level Item cell content.
- Preserve Item-cell line boundaries in description. Encode source line breaks as \n in the JSON string.
- If one Item cell contains multiple sub-items, keep them in one description but format each sub-item on its own line. Prefer Markdown bullets ("- ...") when the source uses bullets, numbering, or line-separated action statements.
- Do not collapse the end of one sub-item and the beginning of the next sub-item into a single space-separated sentence.
- Do not reuse a Jira issue from a previous or next table row. linked_jira_issue must come from the same row only.
- owner_emails and owner_lark_user_ids must come only from the same row's Assignee cell.
- Mentions inside the Item cell are content only. Do not copy Item-cell mentions into owner fields.
- If the section is a non-table list, create at most one JSON object per bullet or paragraph. Do not split one bullet into several actions.
- Preserve source priority values exactly, such as P0, P1, P2, or P3. Do not convert them to Critical, High, Medium, or Low.
- Infer type from the action content. It must be exactly one of: Mitigation SOP, New alert/metric/log, Operation SOP, Healthcheck, ConfigChange.
- Use Mitigation SOP for response or mitigation runbook follow-ups.
- Use New alert/metric/log for adding or improving alerting, metrics, logging, dashboards, or observability signals.
- Use Operation SOP for operational process or manual operation procedure follow-ups.
- Use Healthcheck for health check, validation, inspection, or recurring review follow-ups.
- Use ConfigChange for configuration, threshold, limit, policy, or environment setting changes.
- Infer priority only when the action content supports it and the source Priority cell is empty.
- Owners must be high-confidence. Use email if the Assignee cell has one. Use lark user id only when the Assignee cell contains @lark_user_id:<id>. If owner is ambiguous, leave both owner fields empty.
- Do not invent owners, deadlines, Jira issues, or tasks.
- Output JSON only, no markdown fence.

Document:
%s`, documentContent)
}

func parseExtractedPostMortemActions(raw string) ([]extractedPostMortemAction, error) {
	trimmed, err := extractLLMJSONArray(raw)
	if err != nil {
		return nil, err
	}
	var actions []extractedPostMortemAction
	if err := json.Unmarshal([]byte(trimmed), &actions); err != nil {
		return nil, fmt.Errorf("parse llm extracted todo actions failed: %w", err)
	}
	return actions, nil
}

func (s *incidentService) extractPostMortemTimelineLogs(ctx context.Context, documentContent string) ([]*entity.IncidentLogItem, error) {
	if s.llmCli == nil {
		return nil, fmt.Errorf("llm client is not configured")
	}
	llmCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
	defer cancel()

	resp, err := s.llmCli.GenerateText(llmCtx, buildPostMortemTimelineExtractionPrompt(documentContent))
	if err != nil {
		return nil, fmt.Errorf("extract timeline logs with llm failed: %w", err)
	}
	logs, err := parseExtractedPostMortemTimelineLogs(resp)
	if err != nil {
		return nil, err
	}
	return logs, nil
}

func buildPostMortemTimelineExtractionPrompt(documentContent string) string {
	return fmt.Sprintf(`You extract incident event timeline logs from a Feishu postmortem document.

Read only the "Timeline" section. Use the incident metadata before the Timeline section only to infer missing dates.
Return strict JSON only, with this shape:
[
  {
    "event_time": "RFC3339 timestamp with explicit timezone offset, e.g. 2026-05-08T07:54:00+08:00",
    "description": "concise event description"
  }
]

Rules:
- Extract every timeline entry timestamp in the Timeline section, usually the leading bold timestamp or leading timestamp before a bullet.
- Do not create separate events from incidental times or duration ranges inside an entry description, such as "11:00~11:15".
- Preserve the timezone stated by the source timeline. For example, "utc+8" must become "+08:00"; UTC 0 must become "+00:00".
- If the timeline has time and timezone but no date, infer the local calendar date from the incident start/end metadata or nearby timeline context.
- Do not convert a local timeline time to another timezone. Return the RFC3339 timestamp in the timezone used by that timeline entry.
- Keep descriptions factual and concise. Include important action/result context from the same timeline bullet.
- Ignore images and placeholder instructional text.
- If there is no Timeline section or no extractable time node, return [].
- Do not invent time nodes, dates, or facts.
- Output JSON only, no markdown fence.

Document:
%s`, documentContent)
}

func parseExtractedPostMortemTimelineLogs(raw string) ([]*entity.IncidentLogItem, error) {
	trimmed, err := extractLLMJSONArray(raw)
	if err != nil {
		return nil, err
	}
	var extractedLogs []extractedPostMortemTimelineLog
	if err := json.Unmarshal([]byte(trimmed), &extractedLogs); err != nil {
		return nil, fmt.Errorf("parse llm extracted timeline logs failed: %w", err)
	}

	logs := make([]*entity.IncidentLogItem, 0, len(extractedLogs))
	for _, extractedLog := range extractedLogs {
		eventTimeRaw := strings.TrimSpace(extractedLog.EventTime)
		description := strings.TrimSpace(extractedLog.Description)
		if eventTimeRaw == "" && description == "" {
			continue
		}
		eventTime, err := parseExtractedTimelineEventTime(eventTimeRaw)
		if err != nil {
			return nil, fmt.Errorf("parse extracted timeline event time %q failed: %w", extractedLog.EventTime, err)
		}
		if description == "" {
			return nil, fmt.Errorf("timeline log description is empty for event time %q", extractedLog.EventTime)
		}
		logs = append(logs, &entity.IncidentLogItem{
			ItemType:    entity.LogItemTypeGeneral,
			Description: description,
			From:        "postmortem",
			EventTime:   eventTime,
		})
	}
	return logs, nil
}

func parseExtractedTimelineEventTime(raw string) (*time.Time, error) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return nil, fmt.Errorf("event time is empty")
	}
	layouts := []string{
		time.RFC3339,
		"2006-01-02T15:04-07:00",
		"2006-01-02 15:04:05 -07:00",
		"2006-01-02 15:04 -07:00",
	}
	for _, layout := range layouts {
		parsed, err := time.Parse(layout, trimmed)
		if err == nil {
			return &parsed, nil
		}
	}
	return nil, fmt.Errorf("unsupported timestamp format")
}

func extractLLMJSONArray(raw string) (string, error) {
	trimmed := strings.TrimSpace(raw)
	trimmed = strings.TrimPrefix(trimmed, "```json")
	trimmed = strings.TrimPrefix(trimmed, "```")
	trimmed = strings.TrimSuffix(trimmed, "```")
	trimmed = strings.TrimSpace(trimmed)
	if !strings.HasPrefix(trimmed, "[") {
		match := regexp.MustCompile(`(?s)\[.*\]`).FindString(trimmed)
		if match == "" {
			return "", fmt.Errorf("llm response does not contain a json array: %s", raw)
		}
		trimmed = match
	}
	return trimmed, nil
}

func extractActionItemsSection(documentContent string) string {
	actionSectionHeadings := []string{"action list", "action items"}
	if section, ok := extractMarkdownSectionByHeadings(documentContent, actionSectionHeadings); ok {
		return section
	}
	return documentContent
}

func extractTimelineExtractionContext(documentContent string) string {
	timelineSection, ok := extractMarkdownSectionByHeadings(documentContent, []string{"timeline"})
	if !ok {
		return ""
	}
	header := extractDocumentHeaderBeforeFirstHeading(documentContent)
	return strings.TrimSpace(strings.TrimSpace(header) + "\n\n" + timelineSection)
}

func extractDocumentHeaderBeforeFirstHeading(documentContent string) string {
	lines := strings.Split(documentContent, "\n")
	headingRE := regexp.MustCompile(`^(#+)\s+(.+)$`)
	for i, line := range lines {
		if headingRE.MatchString(strings.TrimSpace(line)) {
			return strings.TrimSpace(strings.Join(lines[:i], "\n"))
		}
	}
	return ""
}

func extractMarkdownSectionByHeadings(documentContent string, targetHeadings []string) (string, bool) {
	lines := strings.Split(documentContent, "\n")
	headingRE := regexp.MustCompile(`^(#+)\s+(.+)$`)
	start := -1
	startLevel := 0
	for _, targetHeading := range targetHeadings {
		for i, line := range lines {
			match := headingRE.FindStringSubmatch(strings.TrimSpace(line))
			if len(match) != 3 {
				continue
			}
			heading := strings.ToLower(strings.TrimSpace(match[2]))
			if heading == targetHeading {
				start = i
				startLevel = len(match[1])
				break
			}
		}
		if start != -1 {
			break
		}
	}
	if start == -1 {
		return "", false
	}
	end := len(lines)
	for i := start + 1; i < len(lines); i++ {
		match := headingRE.FindStringSubmatch(strings.TrimSpace(lines[i]))
		if len(match) == 3 && len(match[1]) <= startLevel {
			end = i
			break
		}
	}
	return strings.TrimSpace(strings.Join(lines[start:end], "\n")), true
}

var larkUserIDPattern = regexp.MustCompile(`@lark_user_id:[A-Za-z0-9_\-]+`)

func stripLarkUserMentions(line string) string {
	return strings.TrimSpace(larkUserIDPattern.ReplaceAllString(line, ""))
}

func parseExtractedDeadline(raw string) (*time.Time, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil, nil
	}
	layouts := []string{
		"2006-01-02",
		time.RFC3339,
		"2006/01/02",
		"Jan 2, 2006",
		"January 2, 2006",
	}
	var lastErr error
	for _, layout := range layouts {
		t, err := time.Parse(layout, raw)
		if err == nil {
			normalized := t.UTC().Truncate(time.Second)
			return &normalized, nil
		}
		lastErr = err
	}
	return nil, lastErr
}

func (s *incidentService) resolveExtractedActionOwners(ctx context.Context, action extractedPostMortemAction) []string {
	seen := map[string]struct{}{}
	seenLarkIDs := map[string]struct{}{}
	emails := make([]string, 0, len(action.OwnerEmails)+len(action.OwnerLarkUserIDs))
	for _, email := range action.OwnerEmails {
		email = strings.TrimSpace(email)
		if email == "" {
			continue
		}
		if _, ok := seen[email]; ok {
			continue
		}
		seen[email] = struct{}{}
		emails = append(emails, email)
	}
	for _, larkID := range action.OwnerLarkUserIDs {
		larkID = strings.TrimSpace(strings.TrimPrefix(larkID, "@lark_user_id:"))
		if larkID == "" {
			continue
		}
		if _, ok := seenLarkIDs[larkID]; ok {
			continue
		}
		seenLarkIDs[larkID] = struct{}{}
		userResp, err := s.userSvc.ListUsers(ctx, &userv1.ListUsersRequest{
			LarkUserId: larkID,
			Page:       1,
			PerPage:    1,
		})
		if err != nil {
			s.Logger(ctx).Error(err, "cannot resolve extracted lark owner", "lark_user_id", larkID)
			continue
		}
		if len(userResp.Users) == 0 {
			continue
		}
		user := userResp.Users[0]
		if user.Email == "" {
			continue
		}
		if _, ok := seen[user.Email]; ok {
			continue
		}
		seen[user.Email] = struct{}{}
		emails = append(emails, user.Email)
	}
	return emails
}

func optionalTrimmedString(raw string) *string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil
	}
	return &raw
}
