from __future__ import annotations

import json
import re
import unicodedata
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any

from ..config import ROOT


KNOWLEDGE_DIR = ROOT / "knowledge"
SOURCES_DIR = KNOWLEDGE_DIR / "sources"
LANCEDB_DIR = KNOWLEDGE_DIR / "lancedb"
RULES_PATH = KNOWLEDGE_DIR / "rules.jsonl"
IMPORTS_PATH = KNOWLEDGE_DIR / "imports.jsonl"
REVIEW_STATE_PATH = KNOWLEDGE_DIR / "review_state.json"
ALLOWED_STATUSES = {"active", "draft", "archived", "duplicate"}
DEFAULT_STATUS = "active"


@dataclass(frozen=True)
class KnowledgeRule:
    id: str
    status: str
    topic: str
    task_ids: list[str]
    suggested_task_ids: list[str]
    rule_type: str
    condition: str
    recommendation: str
    risk: str
    source: str
    source_file: str
    confidence: str
    created_at: str
    updated_at: str
    duplicate_of: str
    supersedes: list[str]
    text: str
    raw: dict[str, Any]

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "KnowledgeRule":
        task_ids = normalize_task_id_list(data.get("task_ids") or [])
        suggested_task_ids = normalize_task_id_list(
            data.get("suggested_task_ids") or data.get("pending_task_ids") or []
        )
        text = str(data.get("text") or "").strip()
        if not text:
            text = " ".join(
                str(data.get(key) or "").strip()
                for key in ("condition", "recommendation", "risk")
                if str(data.get(key) or "").strip()
            )
        return cls(
            id=str(data.get("id") or "").strip(),
            status=normalize_status(data.get("status")),
            topic=str(data.get("topic") or "").strip(),
            task_ids=task_ids,
            suggested_task_ids=suggested_task_ids,
            rule_type=str(data.get("rule_type") or "").strip(),
            condition=str(data.get("condition") or "").strip(),
            recommendation=str(data.get("recommendation") or "").strip(),
            risk=str(data.get("risk") or "").strip(),
            source=str(data.get("source") or "").strip(),
            source_file=str(data.get("source_file") or "").strip(),
            confidence=str(data.get("confidence") or "").strip(),
            created_at=str(data.get("created_at") or "").strip(),
            updated_at=str(data.get("updated_at") or "").strip(),
            duplicate_of=str(data.get("duplicate_of") or "").strip(),
            supersedes=normalize_string_list(data.get("supersedes") or []),
            text=text,
            raw=data,
        )


@dataclass(frozen=True)
class SearchResult:
    rule: KnowledgeRule
    score: int


def ensure_knowledge_store() -> dict[str, Path]:
    KNOWLEDGE_DIR.mkdir(parents=True, exist_ok=True)
    SOURCES_DIR.mkdir(parents=True, exist_ok=True)
    LANCEDB_DIR.mkdir(parents=True, exist_ok=True)
    for path in (RULES_PATH, IMPORTS_PATH):
        if not path.exists():
            path.write_text("", encoding="utf-8")
    if not REVIEW_STATE_PATH.exists():
        REVIEW_STATE_PATH.write_text("{}", encoding="utf-8")
    return {
        "knowledge_dir": KNOWLEDGE_DIR,
        "sources_dir": SOURCES_DIR,
        "lancedb_dir": LANCEDB_DIR,
        "rules_path": RULES_PATH,
        "imports_path": IMPORTS_PATH,
        "review_state_path": REVIEW_STATE_PATH,
    }


def load_rules(path: Path = RULES_PATH) -> list[KnowledgeRule]:
    ensure_knowledge_store()
    rows = read_rule_rows(path)
    return [KnowledgeRule.from_dict(row) for row in rows]


def read_rule_rows(path: Path = RULES_PATH) -> list[dict[str, Any]]:
    ensure_knowledge_store()
    rows: list[dict[str, Any]] = []
    for line_number, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
        line = raw_line.strip()
        if not line:
            continue
        try:
            data = json.loads(line)
        except json.JSONDecodeError as exc:
            raise ValueError(f"Nieprawidlowy JSONL w {path}:{line_number}: {exc}") from exc
        if not isinstance(data, dict):
            raise ValueError(f"Wiersz {path}:{line_number} nie jest obiektem JSON.")
        rows.append(data)
    return rows


def write_rule_rows(rows: list[dict[str, Any]], path: Path = RULES_PATH) -> None:
    ensure_knowledge_store()
    with path.open("w", encoding="utf-8", newline="\n") as f:
        for row in rows:
            f.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n")


def append_rules(
    rule_rows: list[dict[str, Any]],
    path: Path = RULES_PATH,
    source_file: str = "",
) -> list[KnowledgeRule]:
    ensure_knowledge_store()
    existing_ids = {rule.id for rule in load_rules(path) if rule.id}
    normalized_rows = []
    for row in rule_rows:
        if source_file and not row.get("source_file"):
            row = {**row, "source_file": source_file}
        normalized = normalize_rule_row(row)
        normalized["id"] = unique_rule_id(normalized["id"], existing_ids)
        existing_ids.add(normalized["id"])
        normalized_rows.append(normalized)

    if normalized_rows:
        with path.open("a", encoding="utf-8", newline="\n") as f:
            for row in normalized_rows:
                f.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n")
    return [KnowledgeRule.from_dict(row) for row in normalized_rows]


def append_import_record(record: dict[str, Any], path: Path = IMPORTS_PATH) -> None:
    ensure_knowledge_store()
    payload = {
        "created_at": datetime.now().isoformat(timespec="seconds"),
        **record,
    }
    with path.open("a", encoding="utf-8", newline="\n") as f:
        f.write(json.dumps(payload, ensure_ascii=False, sort_keys=True) + "\n")


def rules_for_task(task_id: str, path: Path = RULES_PATH) -> list[KnowledgeRule]:
    task_id = task_id.strip()
    return [
        rule
        for rule in load_rules(path)
        if rule.status == "active" and task_id in rule.task_ids
    ]


def search_rules(query: str, limit: int = 10, path: Path = RULES_PATH) -> list[SearchResult]:
    query_terms = tokenize(query)
    if not query_terms:
        return []

    results: list[SearchResult] = []
    for rule in load_rules(path):
        if rule.status != "active":
            continue
        haystack = searchable_text(rule)
        score = sum(haystack.count(term) for term in query_terms)
        if rule.topic.casefold() in query.casefold():
            score += 3
        for task_id in rule.task_ids:
            if task_id.casefold() in query.casefold():
                score += 3
        if score > 0:
            results.append(SearchResult(rule=rule, score=score))

    results.sort(key=lambda item: (-item.score, item.rule.topic, item.rule.id))
    return results[: max(limit, 0)]


def store_summary(path: Path = RULES_PATH) -> dict[str, int]:
    rules = load_rules(path)
    topics = {rule.topic for rule in rules if rule.topic}
    task_ids = {task_id for rule in rules for task_id in rule.task_ids}
    suggested_task_ids = {
        task_id
        for rule in rules
        for task_id in rule.suggested_task_ids
        if task_id not in rule.task_ids
    }
    sources = {rule.source for rule in rules if rule.source}
    by_status = {status: sum(1 for rule in rules if rule.status == status) for status in ALLOWED_STATUSES}
    return {
        "rules": len(rules),
        "active_rules": by_status.get("active", 0),
        "draft_rules": by_status.get("draft", 0),
        "archived_rules": by_status.get("archived", 0),
        "duplicate_rules": by_status.get("duplicate", 0),
        "topics": len(topics),
        "task_ids": len(task_ids),
        "suggested_task_ids": len(suggested_task_ids),
        "suggestions": sum(
            1
            for rule in rules
            for task_id in rule.suggested_task_ids
            if task_id not in rule.task_ids
        ),
        "sources": len(sources),
    }


def list_rules(
    topic: str | None = None,
    task_id: str | None = None,
    status: str | None = None,
    source: str | None = None,
    path: Path = RULES_PATH,
) -> list[KnowledgeRule]:
    rules = load_rules(path)
    if topic:
        normalized_topic = slugify(topic)
        rules = [rule for rule in rules if rule.topic == normalized_topic]
    if task_id:
        normalized_task_id = slugify(task_id)
        rules = [rule for rule in rules if normalized_task_id in rule.task_ids]
    if status:
        normalized_status = normalize_status(status)
        rules = [rule for rule in rules if rule.status == normalized_status]
    if source:
        normalized_source = source.casefold()
        rules = [rule for rule in rules if normalized_source in rule.source.casefold()]
    return sorted(rules, key=lambda rule: (rule.topic, rule.id))


def update_rule_status(
    rule_id: str,
    status: str,
    duplicate_of: str = "",
    path: Path = RULES_PATH,
) -> KnowledgeRule:
    normalized_rule_id = rule_id.strip()
    normalized_status = normalize_status(status)
    if normalized_status not in ALLOWED_STATUSES:
        raise ValueError(f"Nieprawidlowy status: {status}")

    rows = read_rule_rows(path)
    updated_rule: KnowledgeRule | None = None
    now = now_iso()
    for row in rows:
        if str(row.get("id") or "").strip() != normalized_rule_id:
            continue
        row["status"] = normalized_status
        row["updated_at"] = now
        if duplicate_of:
            row["duplicate_of"] = duplicate_of.strip()
        elif normalized_status != "duplicate":
            row["duplicate_of"] = ""
        updated_rule = KnowledgeRule.from_dict(row)
        break

    if not updated_rule:
        raise ValueError(f"Nie znaleziono reguly: {normalized_rule_id}")

    write_rule_rows(rows, path)
    return updated_rule


def tokenize(value: str) -> list[str]:
    return [
        token.casefold()
        for token in re.findall(r"\w+", value or "", flags=re.UNICODE)
        if len(token) >= 2
    ]


def searchable_text(rule: KnowledgeRule) -> str:
    values = [
        rule.id,
        rule.status,
        rule.topic,
        " ".join(rule.task_ids),
        " ".join(rule.suggested_task_ids),
        rule.rule_type,
        rule.condition,
        rule.recommendation,
        rule.risk,
        rule.source,
        rule.source_file,
        rule.confidence,
        rule.text,
    ]
    return " ".join(values).casefold()


def normalize_rule_row(data: dict[str, Any]) -> dict[str, Any]:
    task_ids = normalize_task_id_list(data.get("task_ids") or [])
    suggested_task_ids = normalize_task_id_list(data.get("suggested_task_ids") or [])
    created_at = clean_field(data.get("created_at")) or now_iso()
    updated_at = clean_field(data.get("updated_at")) or created_at
    text = clean_field(data.get("text"))
    condition = clean_field(data.get("condition"))
    recommendation = clean_field(data.get("recommendation"))
    risk = clean_field(data.get("risk"))
    if not text:
        text = " ".join(item for item in [condition, recommendation, risk] if item)
    row = {
        "id": slugify(clean_field(data.get("id")) or text or recommendation or condition or "knowledge_rule"),
        "status": normalize_status(data.get("status")),
        "topic": slugify(clean_field(data.get("topic")) or "inne"),
        "task_ids": task_ids,
        "suggested_task_ids": [item for item in suggested_task_ids if item not in task_ids],
        "rule_type": slugify(clean_field(data.get("rule_type")) or "recommendation"),
        "condition": condition,
        "recommendation": recommendation,
        "risk": risk,
        "source": clean_field(data.get("source")),
        "source_file": clean_field(data.get("source_file")),
        "confidence": slugify(clean_field(data.get("confidence")) or "medium"),
        "created_at": created_at,
        "updated_at": updated_at,
        "duplicate_of": clean_field(data.get("duplicate_of")),
        "supersedes": normalize_string_list(data.get("supersedes") or []),
        "text": text,
    }
    validate_rule_row(row)
    return row


def validate_rule_row(row: dict[str, Any]) -> None:
    required = [
        "id",
        "status",
        "topic",
        "task_ids",
        "suggested_task_ids",
        "rule_type",
        "condition",
        "recommendation",
        "risk",
        "source",
        "source_file",
        "confidence",
        "created_at",
        "updated_at",
        "duplicate_of",
        "supersedes",
        "text",
    ]
    missing = [key for key in required if key not in row]
    if missing:
        raise ValueError(f"Regula nie ma wymaganych pol: {', '.join(missing)}")
    if not isinstance(row["task_ids"], list):
        raise ValueError("Pole task_ids musi byc lista.")
    if not isinstance(row["suggested_task_ids"], list):
        raise ValueError("Pole suggested_task_ids musi byc lista.")
    if not isinstance(row["supersedes"], list):
        raise ValueError("Pole supersedes musi byc lista.")
    if not row["id"]:
        raise ValueError("Regula musi miec id.")
    if row["status"] not in ALLOWED_STATUSES:
        raise ValueError(f"Nieprawidlowy status reguly: {row['status']}")
    if not row["topic"]:
        raise ValueError("Regula musi miec topic.")
    if not row["source"]:
        raise ValueError("Regula musi miec source.")
    if not row["text"] and not row["recommendation"]:
        raise ValueError("Regula musi miec text albo recommendation.")


def clean_field(value: Any) -> str:
    return " ".join(str(value or "").replace("\r", "\n").split())


def now_iso() -> str:
    return datetime.now().isoformat(timespec="seconds")


def normalize_status(value: Any) -> str:
    status = slugify(clean_field(value) or DEFAULT_STATUS)
    if status not in ALLOWED_STATUSES:
        return DEFAULT_STATUS
    return status


def normalize_task_id_list(value: Any) -> list[str]:
    if isinstance(value, str):
        value = [value]
    if not isinstance(value, list):
        return []
    return [slugify(str(item)) for item in value if str(item).strip()]


def normalize_string_list(value: Any) -> list[str]:
    if isinstance(value, str):
        value = [value]
    if not isinstance(value, list):
        return []
    return [clean_field(item) for item in value if clean_field(item)]


def slugify(value: str, max_length: int = 96) -> str:
    value = value.casefold()
    replacements = {"\u0142": "l"}
    for source, target in replacements.items():
        value = value.replace(source, target)
    value = unicodedata.normalize("NFKD", value)
    value = "".join(char for char in value if not unicodedata.combining(char))
    value = re.sub(r"[^a-z0-9]+", "_", value).strip("_")
    value = re.sub(r"_+", "_", value)
    return (value or "knowledge_rule")[:max_length].strip("_") or "knowledge_rule"


def unique_rule_id(base_id: str, existing_ids: set[str]) -> str:
    candidate = base_id
    suffix = 2
    while candidate in existing_ids:
        candidate = f"{base_id}_{suffix}"
        suffix += 1
    return candidate


def pending_task_suggestions(path: Path = RULES_PATH) -> list[dict[str, Any]]:
    suggestions: list[dict[str, Any]] = []
    for rule in load_rules(path):
        if rule.status != "active":
            continue
        for task_id in rule.suggested_task_ids:
            if task_id in rule.task_ids:
                continue
            suggestions.append({"rule": rule, "task_id": task_id})
    return suggestions


def approve_task_suggestion(rule_id: str, task_id: str, path: Path = RULES_PATH) -> KnowledgeRule:
    return update_task_suggestion(rule_id, task_id, approve=True, path=path)


def reject_task_suggestion(rule_id: str, task_id: str, path: Path = RULES_PATH) -> KnowledgeRule:
    return update_task_suggestion(rule_id, task_id, approve=False, path=path)


def update_task_suggestion(
    rule_id: str,
    task_id: str,
    approve: bool,
    path: Path = RULES_PATH,
) -> KnowledgeRule:
    normalized_rule_id = rule_id.strip()
    normalized_task_id = slugify(task_id)
    rows = read_rule_rows(path)
    updated_rule: KnowledgeRule | None = None

    for row in rows:
        if str(row.get("id") or "").strip() != normalized_rule_id:
            continue
        task_ids = normalize_task_id_list(row.get("task_ids") or [])
        suggested_task_ids = normalize_task_id_list(row.get("suggested_task_ids") or [])
        if normalized_task_id not in suggested_task_ids:
            raise ValueError(
                f"Regula {normalized_rule_id} nie ma oczekujacej propozycji dla zadania {normalized_task_id}."
            )
        if approve and normalized_task_id not in task_ids:
            task_ids.append(normalized_task_id)
        row["task_ids"] = task_ids
        row["suggested_task_ids"] = [item for item in suggested_task_ids if item != normalized_task_id]
        row["updated_at"] = now_iso()
        updated_rule = KnowledgeRule.from_dict(row)
        break

    if not updated_rule:
        raise ValueError(f"Nie znaleziono reguly: {normalized_rule_id}")

    write_rule_rows(rows, path)
    return updated_rule


def assign_rule_to_task(rule_id: str, task_id: str, path: Path = RULES_PATH) -> KnowledgeRule:
    normalized_rule_id = rule_id.strip()
    normalized_task_id = slugify(task_id)
    rows = read_rule_rows(path)
    updated_rule: KnowledgeRule | None = None

    for row in rows:
        if str(row.get("id") or "").strip() != normalized_rule_id:
            continue
        task_ids = normalize_task_id_list(row.get("task_ids") or [])
        suggested_task_ids = normalize_task_id_list(row.get("suggested_task_ids") or [])
        if normalized_task_id not in task_ids:
            task_ids.append(normalized_task_id)
        row["task_ids"] = task_ids
        row["suggested_task_ids"] = [item for item in suggested_task_ids if item != normalized_task_id]
        row["updated_at"] = now_iso()
        updated_rule = KnowledgeRule.from_dict(row)
        break

    if not updated_rule:
        raise ValueError(f"Nie znaleziono reguly: {normalized_rule_id}")

    write_rule_rows(rows, path)
    return updated_rule


def delete_rule(rule_id: str, path: Path = RULES_PATH) -> KnowledgeRule:
    normalized_rule_id = rule_id.strip()
    rows = read_rule_rows(path)
    remaining_rows: list[dict[str, Any]] = []
    deleted_rule: KnowledgeRule | None = None

    for row in rows:
        if str(row.get("id") or "").strip() == normalized_rule_id and deleted_rule is None:
            deleted_rule = KnowledgeRule.from_dict(row)
            continue
        remaining_rows.append(row)

    if not deleted_rule:
        raise ValueError(f"Nie znaleziono reguly: {normalized_rule_id}")

    write_rule_rows(remaining_rows, path)
    return deleted_rule


def unassigned_rules(path: Path = RULES_PATH) -> list[KnowledgeRule]:
    return [
        rule
        for rule in load_rules(path)
        if rule.status == "active" and not rule.task_ids
    ]


def load_review_state(path: Path = REVIEW_STATE_PATH) -> dict[str, Any]:
    ensure_knowledge_store()
    if not path.exists():
        return {}
    raw = path.read_text(encoding="utf-8").strip()
    if not raw:
        return {}
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return {}
    return data if isinstance(data, dict) else {}


def save_review_state(state: dict[str, Any], path: Path = REVIEW_STATE_PATH) -> None:
    ensure_knowledge_store()
    path.write_text(json.dumps(state, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8")


def reset_review_state(queue_name: str = "unassigned") -> None:
    state = load_review_state()
    state.pop(queue_name, None)
    save_review_state(state)


def review_start_index(rules: list[KnowledgeRule], queue_name: str = "unassigned") -> int:
    state = load_review_state()
    queue_state = state.get(queue_name) or {}
    last_rule_id = queue_state.get("last_rule_id", "")
    if not last_rule_id:
        return 0
    for index, rule in enumerate(rules):
        if rule.id == last_rule_id:
            return min(index + 1, len(rules))
    last_queue_index = queue_state.get("last_queue_index")
    if isinstance(last_queue_index, int):
        return min(max(last_queue_index, 0), len(rules))
    last_sort_key = queue_state.get("last_sort_key")
    if isinstance(last_sort_key, list) and len(last_sort_key) == 2:
        normalized_last_sort_key = (str(last_sort_key[0]), str(last_sort_key[1]))
        for index, rule in enumerate(rules):
            if (rule.topic, rule.id) > normalized_last_sort_key:
                return index
        return len(rules)
    return 0


def mark_review_progress(
    rule_id: str,
    queue_name: str = "unassigned",
    sort_key: list[str] | None = None,
    queue_index: int | None = None,
) -> None:
    state = load_review_state()
    queue_state = {
        "last_rule_id": rule_id,
        "updated_at": now_iso(),
    }
    if sort_key:
        queue_state["last_sort_key"] = [str(sort_key[0]), str(sort_key[1])]
    if queue_index is not None:
        queue_state["last_queue_index"] = queue_index
    state[queue_name] = queue_state
    save_review_state(state)


def rule_preview(rule: KnowledgeRule, max_length: int = 90) -> str:
    value = rule.recommendation or rule.text or rule.condition or rule.risk
    value = " ".join(value.split())
    if len(value) <= max_length:
        return value
    return value[: max_length - 3].rstrip() + "..."
